summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc21
-rw-r--r--.eslintrc2
-rw-r--r--.flayignore1
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml64
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md10
-rw-r--r--.rubocop.yml273
-rw-r--r--.rubocop_todo.yml317
-rw-r--r--CHANGELOG.md229
-rw-r--r--CONTRIBUTING.md121
-rw-r--r--GITALY_SERVER_VERSION1
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile123
-rw-r--r--Gemfile.lock97
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/emoji.pngbin1087659 -> 1218558 bytes
-rw-r--r--app/assets/images/emoji/100.pngbin0 -> 793 bytes
-rw-r--r--app/assets/images/emoji/1234.pngbin0 -> 676 bytes
-rw-r--r--app/assets/images/emoji/1F627.pngbin0 -> 821 bytes
-rw-r--r--app/assets/images/emoji/8ball.pngbin0 -> 810 bytes
-rw-r--r--app/assets/images/emoji/a.pngbin0 -> 469 bytes
-rw-r--r--app/assets/images/emoji/ab.pngbin0 -> 505 bytes
-rw-r--r--app/assets/images/emoji/abc.pngbin0 -> 646 bytes
-rw-r--r--app/assets/images/emoji/abcd.pngbin0 -> 670 bytes
-rw-r--r--app/assets/images/emoji/accept.pngbin0 -> 491 bytes
-rw-r--r--app/assets/images/emoji/aerial_tramway.pngbin0 -> 759 bytes
-rw-r--r--app/assets/images/emoji/airplane.pngbin0 -> 1152 bytes
-rw-r--r--app/assets/images/emoji/airplane_arriving.pngbin0 -> 1101 bytes
-rw-r--r--app/assets/images/emoji/airplane_departure.pngbin0 -> 1111 bytes
-rw-r--r--app/assets/images/emoji/airplane_small.pngbin0 -> 1229 bytes
-rw-r--r--app/assets/images/emoji/alarm_clock.pngbin0 -> 1044 bytes
-rw-r--r--app/assets/images/emoji/alembic.pngbin0 -> 953 bytes
-rw-r--r--app/assets/images/emoji/alien.pngbin0 -> 839 bytes
-rw-r--r--app/assets/images/emoji/ambulance.pngbin0 -> 1238 bytes
-rw-r--r--app/assets/images/emoji/amphora.pngbin0 -> 1044 bytes
-rw-r--r--app/assets/images/emoji/anchor.pngbin0 -> 779 bytes
-rw-r--r--app/assets/images/emoji/angel.pngbin0 -> 2077 bytes
-rw-r--r--app/assets/images/emoji/angel_tone1.pngbin0 -> 2088 bytes
-rw-r--r--app/assets/images/emoji/angel_tone2.pngbin0 -> 2075 bytes
-rw-r--r--app/assets/images/emoji/angel_tone3.pngbin0 -> 2078 bytes
-rw-r--r--app/assets/images/emoji/angel_tone4.pngbin0 -> 2076 bytes
-rw-r--r--app/assets/images/emoji/angel_tone5.pngbin0 -> 2078 bytes
-rw-r--r--app/assets/images/emoji/anger.pngbin0 -> 594 bytes
-rw-r--r--app/assets/images/emoji/anger_right.pngbin0 -> 551 bytes
-rw-r--r--app/assets/images/emoji/angry.pngbin0 -> 845 bytes
-rw-r--r--app/assets/images/emoji/ant.pngbin0 -> 1412 bytes
-rw-r--r--app/assets/images/emoji/apple.pngbin0 -> 655 bytes
-rw-r--r--app/assets/images/emoji/aquarius.pngbin0 -> 648 bytes
-rw-r--r--app/assets/images/emoji/aries.pngbin0 -> 711 bytes
-rw-r--r--app/assets/images/emoji/arrow_backward.pngbin0 -> 429 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_down.pngbin0 -> 543 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_up.pngbin0 -> 535 bytes
-rw-r--r--app/assets/images/emoji/arrow_down.pngbin0 -> 512 bytes
-rw-r--r--app/assets/images/emoji/arrow_down_small.pngbin0 -> 455 bytes
-rw-r--r--app/assets/images/emoji/arrow_forward.pngbin0 -> 429 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_down.pngbin0 -> 563 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_up.pngbin0 -> 559 bytes
-rw-r--r--app/assets/images/emoji/arrow_left.pngbin0 -> 471 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_left.pngbin0 -> 520 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_right.pngbin0 -> 526 bytes
-rw-r--r--app/assets/images/emoji/arrow_right.pngbin0 -> 468 bytes
-rw-r--r--app/assets/images/emoji/arrow_right_hook.pngbin0 -> 644 bytes
-rw-r--r--app/assets/images/emoji/arrow_up.pngbin0 -> 507 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_down.pngbin0 -> 474 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_small.pngbin0 -> 454 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_left.pngbin0 -> 521 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_right.pngbin0 -> 524 bytes
-rw-r--r--app/assets/images/emoji/arrows_clockwise.pngbin0 -> 519 bytes
-rw-r--r--app/assets/images/emoji/arrows_counterclockwise.pngbin0 -> 693 bytes
-rw-r--r--app/assets/images/emoji/art.pngbin0 -> 1455 bytes
-rw-r--r--app/assets/images/emoji/articulated_lorry.pngbin0 -> 1710 bytes
-rw-r--r--app/assets/images/emoji/asterisk.pngbin0 -> 627 bytes
-rw-r--r--app/assets/images/emoji/astonished.pngbin0 -> 862 bytes
-rw-r--r--app/assets/images/emoji/athletic_shoe.pngbin0 -> 1595 bytes
-rw-r--r--app/assets/images/emoji/atm.pngbin0 -> 1397 bytes
-rw-r--r--app/assets/images/emoji/atom.pngbin0 -> 912 bytes
-rw-r--r--app/assets/images/emoji/avocado.pngbin0 -> 1520 bytes
-rw-r--r--app/assets/images/emoji/b.pngbin0 -> 391 bytes
-rw-r--r--app/assets/images/emoji/baby.pngbin0 -> 1380 bytes
-rw-r--r--app/assets/images/emoji/baby_bottle.pngbin0 -> 818 bytes
-rw-r--r--app/assets/images/emoji/baby_chick.pngbin0 -> 1181 bytes
-rw-r--r--app/assets/images/emoji/baby_symbol.pngbin0 -> 665 bytes
-rw-r--r--app/assets/images/emoji/baby_tone1.pngbin0 -> 1392 bytes
-rw-r--r--app/assets/images/emoji/baby_tone2.pngbin0 -> 1392 bytes
-rw-r--r--app/assets/images/emoji/baby_tone3.pngbin0 -> 1403 bytes
-rw-r--r--app/assets/images/emoji/baby_tone4.pngbin0 -> 1413 bytes
-rw-r--r--app/assets/images/emoji/baby_tone5.pngbin0 -> 1405 bytes
-rw-r--r--app/assets/images/emoji/back.pngbin0 -> 562 bytes
-rw-r--r--app/assets/images/emoji/bacon.pngbin0 -> 2148 bytes
-rw-r--r--app/assets/images/emoji/badminton.pngbin0 -> 1253 bytes
-rw-r--r--app/assets/images/emoji/baggage_claim.pngbin0 -> 490 bytes
-rw-r--r--app/assets/images/emoji/balloon.pngbin0 -> 501 bytes
-rw-r--r--app/assets/images/emoji/ballot_box.pngbin0 -> 1355 bytes
-rw-r--r--app/assets/images/emoji/ballot_box_with_check.pngbin0 -> 639 bytes
-rw-r--r--app/assets/images/emoji/bamboo.pngbin0 -> 1946 bytes
-rw-r--r--app/assets/images/emoji/banana.pngbin0 -> 1157 bytes
-rw-r--r--app/assets/images/emoji/bangbang.pngbin0 -> 390 bytes
-rw-r--r--app/assets/images/emoji/bank.pngbin0 -> 1358 bytes
-rw-r--r--app/assets/images/emoji/bar_chart.pngbin0 -> 408 bytes
-rw-r--r--app/assets/images/emoji/barber.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/baseball.pngbin0 -> 1185 bytes
-rw-r--r--app/assets/images/emoji/basketball.pngbin0 -> 1546 bytes
-rw-r--r--app/assets/images/emoji/basketball_player.pngbin0 -> 1491 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone1.pngbin0 -> 1492 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone2.pngbin0 -> 1493 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone3.pngbin0 -> 1492 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone4.pngbin0 -> 1491 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone5.pngbin0 -> 1474 bytes
-rw-r--r--app/assets/images/emoji/bat.pngbin0 -> 1190 bytes
-rw-r--r--app/assets/images/emoji/bath.pngbin0 -> 1238 bytes
-rw-r--r--app/assets/images/emoji/bath_tone1.pngbin0 -> 1235 bytes
-rw-r--r--app/assets/images/emoji/bath_tone2.pngbin0 -> 1231 bytes
-rw-r--r--app/assets/images/emoji/bath_tone3.pngbin0 -> 1236 bytes
-rw-r--r--app/assets/images/emoji/bath_tone4.pngbin0 -> 1252 bytes
-rw-r--r--app/assets/images/emoji/bath_tone5.pngbin0 -> 1239 bytes
-rw-r--r--app/assets/images/emoji/bathtub.pngbin0 -> 767 bytes
-rw-r--r--app/assets/images/emoji/battery.pngbin0 -> 228 bytes
-rw-r--r--app/assets/images/emoji/beach.pngbin0 -> 942 bytes
-rw-r--r--app/assets/images/emoji/beach_umbrella.pngbin0 -> 1486 bytes
-rw-r--r--app/assets/images/emoji/bear.pngbin0 -> 1023 bytes
-rw-r--r--app/assets/images/emoji/bed.pngbin0 -> 1572 bytes
-rw-r--r--app/assets/images/emoji/bee.pngbin0 -> 1378 bytes
-rw-r--r--app/assets/images/emoji/beer.pngbin0 -> 1338 bytes
-rw-r--r--app/assets/images/emoji/beers.pngbin0 -> 2100 bytes
-rw-r--r--app/assets/images/emoji/beetle.pngbin0 -> 1288 bytes
-rw-r--r--app/assets/images/emoji/beginner.pngbin0 -> 545 bytes
-rw-r--r--app/assets/images/emoji/bell.pngbin0 -> 1496 bytes
-rw-r--r--app/assets/images/emoji/bellhop.pngbin0 -> 891 bytes
-rw-r--r--app/assets/images/emoji/bento.pngbin0 -> 1127 bytes
-rw-r--r--app/assets/images/emoji/bicyclist.pngbin0 -> 1911 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone1.pngbin0 -> 1860 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone2.pngbin0 -> 1866 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone3.pngbin0 -> 1851 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone4.pngbin0 -> 1852 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone5.pngbin0 -> 1840 bytes
-rw-r--r--app/assets/images/emoji/bike.pngbin0 -> 1505 bytes
-rw-r--r--app/assets/images/emoji/bikini.pngbin0 -> 613 bytes
-rw-r--r--app/assets/images/emoji/biohazard.pngbin0 -> 794 bytes
-rw-r--r--app/assets/images/emoji/bird.pngbin0 -> 1068 bytes
-rw-r--r--app/assets/images/emoji/birthday.pngbin0 -> 2219 bytes
-rw-r--r--app/assets/images/emoji/black_circle.pngbin0 -> 374 bytes
-rw-r--r--app/assets/images/emoji/black_heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/black_joker.pngbin0 -> 1091 bytes
-rw-r--r--app/assets/images/emoji/black_large_square.pngbin0 -> 110 bytes
-rw-r--r--app/assets/images/emoji/black_medium_small_square.pngbin0 -> 110 bytes
-rw-r--r--app/assets/images/emoji/black_medium_square.pngbin0 -> 108 bytes
-rw-r--r--app/assets/images/emoji/black_nib.pngbin0 -> 620 bytes
-rw-r--r--app/assets/images/emoji/black_small_square.pngbin0 -> 108 bytes
-rw-r--r--app/assets/images/emoji/black_square_button.pngbin0 -> 122 bytes
-rw-r--r--app/assets/images/emoji/blossom.pngbin0 -> 867 bytes
-rw-r--r--app/assets/images/emoji/blowfish.pngbin0 -> 1620 bytes
-rw-r--r--app/assets/images/emoji/blue_book.pngbin0 -> 1347 bytes
-rw-r--r--app/assets/images/emoji/blue_car.pngbin0 -> 1275 bytes
-rw-r--r--app/assets/images/emoji/blue_heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/blush.pngbin0 -> 812 bytes
-rw-r--r--app/assets/images/emoji/boar.pngbin0 -> 1366 bytes
-rw-r--r--app/assets/images/emoji/bomb.pngbin0 -> 702 bytes
-rw-r--r--app/assets/images/emoji/book.pngbin0 -> 1716 bytes
-rw-r--r--app/assets/images/emoji/bookmark.pngbin0 -> 747 bytes
-rw-r--r--app/assets/images/emoji/bookmark_tabs.pngbin0 -> 1395 bytes
-rw-r--r--app/assets/images/emoji/books.pngbin0 -> 2474 bytes
-rw-r--r--app/assets/images/emoji/boom.pngbin0 -> 1110 bytes
-rw-r--r--app/assets/images/emoji/boot.pngbin0 -> 662 bytes
-rw-r--r--app/assets/images/emoji/bouquet.pngbin0 -> 1662 bytes
-rw-r--r--app/assets/images/emoji/bow.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bow_and_arrow.pngbin0 -> 1402 bytes
-rw-r--r--app/assets/images/emoji/bow_tone1.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bow_tone2.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bow_tone3.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bow_tone4.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bow_tone5.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/bowling.pngbin0 -> 1426 bytes
-rw-r--r--app/assets/images/emoji/boxing_glove.pngbin0 -> 1575 bytes
-rw-r--r--app/assets/images/emoji/boy.pngbin0 -> 881 bytes
-rw-r--r--app/assets/images/emoji/boy_tone1.pngbin0 -> 876 bytes
-rw-r--r--app/assets/images/emoji/boy_tone2.pngbin0 -> 876 bytes
-rw-r--r--app/assets/images/emoji/boy_tone3.pngbin0 -> 876 bytes
-rw-r--r--app/assets/images/emoji/boy_tone4.pngbin0 -> 870 bytes
-rw-r--r--app/assets/images/emoji/boy_tone5.pngbin0 -> 873 bytes
-rw-r--r--app/assets/images/emoji/bread.pngbin0 -> 1419 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil.pngbin0 -> 2452 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone1.pngbin0 -> 2464 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone2.pngbin0 -> 2457 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone3.pngbin0 -> 2463 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone4.pngbin0 -> 2463 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone5.pngbin0 -> 2462 bytes
-rw-r--r--app/assets/images/emoji/bridge_at_night.pngbin0 -> 637 bytes
-rw-r--r--app/assets/images/emoji/briefcase.pngbin0 -> 1275 bytes
-rw-r--r--app/assets/images/emoji/broken_heart.pngbin0 -> 556 bytes
-rw-r--r--app/assets/images/emoji/bug.pngbin0 -> 1599 bytes
-rw-r--r--app/assets/images/emoji/bulb.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_front.pngbin0 -> 1450 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_side.pngbin0 -> 1538 bytes
-rw-r--r--app/assets/images/emoji/burrito.pngbin0 -> 2938 bytes
-rw-r--r--app/assets/images/emoji/bus.pngbin0 -> 1086 bytes
-rw-r--r--app/assets/images/emoji/busstop.pngbin0 -> 626 bytes
-rw-r--r--app/assets/images/emoji/bust_in_silhouette.pngbin0 -> 426 bytes
-rw-r--r--app/assets/images/emoji/busts_in_silhouette.pngbin0 -> 526 bytes
-rw-r--r--app/assets/images/emoji/butterfly.pngbin0 -> 1981 bytes
-rw-r--r--app/assets/images/emoji/cactus.pngbin0 -> 628 bytes
-rw-r--r--app/assets/images/emoji/cake.pngbin0 -> 2266 bytes
-rw-r--r--app/assets/images/emoji/calendar.pngbin0 -> 2077 bytes
-rw-r--r--app/assets/images/emoji/calendar_spiral.pngbin0 -> 1491 bytes
-rw-r--r--app/assets/images/emoji/call_me.pngbin0 -> 894 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone1.pngbin0 -> 893 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone2.pngbin0 -> 891 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone3.pngbin0 -> 891 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone4.pngbin0 -> 891 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone5.pngbin0 -> 893 bytes
-rw-r--r--app/assets/images/emoji/calling.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/camel.pngbin0 -> 1190 bytes
-rw-r--r--app/assets/images/emoji/camera.pngbin0 -> 1783 bytes
-rw-r--r--app/assets/images/emoji/camera_with_flash.pngbin0 -> 2097 bytes
-rw-r--r--app/assets/images/emoji/camping.pngbin0 -> 1513 bytes
-rw-r--r--app/assets/images/emoji/cancer.pngbin0 -> 729 bytes
-rw-r--r--app/assets/images/emoji/candle.pngbin0 -> 1250 bytes
-rw-r--r--app/assets/images/emoji/candy.pngbin0 -> 1054 bytes
-rw-r--r--app/assets/images/emoji/canoe.pngbin0 -> 1244 bytes
-rw-r--r--app/assets/images/emoji/capital_abcd.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/capricorn.pngbin0 -> 688 bytes
-rw-r--r--app/assets/images/emoji/card_box.pngbin0 -> 1523 bytes
-rw-r--r--app/assets/images/emoji/card_index.pngbin0 -> 1929 bytes
-rw-r--r--app/assets/images/emoji/carousel_horse.pngbin0 -> 1739 bytes
-rw-r--r--app/assets/images/emoji/carrot.pngbin0 -> 1236 bytes
-rw-r--r--app/assets/images/emoji/cartwheel.pngbin0 -> 1233 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone1.pngbin0 -> 1234 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone2.pngbin0 -> 1235 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone3.pngbin0 -> 1229 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone4.pngbin0 -> 1227 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone5.pngbin0 -> 1214 bytes
-rw-r--r--app/assets/images/emoji/cat.pngbin0 -> 1354 bytes
-rw-r--r--app/assets/images/emoji/cat2.pngbin0 -> 1781 bytes
-rw-r--r--app/assets/images/emoji/cd.pngbin0 -> 908 bytes
-rw-r--r--app/assets/images/emoji/chains.pngbin0 -> 708 bytes
-rw-r--r--app/assets/images/emoji/champagne.pngbin0 -> 1205 bytes
-rw-r--r--app/assets/images/emoji/champagne_glass.pngbin0 -> 1984 bytes
-rw-r--r--app/assets/images/emoji/chart.pngbin0 -> 724 bytes
-rw-r--r--app/assets/images/emoji/chart_with_downwards_trend.pngbin0 -> 709 bytes
-rw-r--r--app/assets/images/emoji/chart_with_upwards_trend.pngbin0 -> 688 bytes
-rw-r--r--app/assets/images/emoji/checkered_flag.pngbin0 -> 787 bytes
-rw-r--r--app/assets/images/emoji/cheese.pngbin0 -> 1697 bytes
-rw-r--r--app/assets/images/emoji/cherries.pngbin0 -> 1211 bytes
-rw-r--r--app/assets/images/emoji/cherry_blossom.pngbin0 -> 1129 bytes
-rw-r--r--app/assets/images/emoji/chestnut.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/emoji/chicken.pngbin0 -> 1267 bytes
-rw-r--r--app/assets/images/emoji/children_crossing.pngbin0 -> 778 bytes
-rw-r--r--app/assets/images/emoji/chipmunk.pngbin0 -> 1454 bytes
-rw-r--r--app/assets/images/emoji/chocolate_bar.pngbin0 -> 771 bytes
-rw-r--r--app/assets/images/emoji/christmas_tree.pngbin0 -> 1542 bytes
-rw-r--r--app/assets/images/emoji/church.pngbin0 -> 1298 bytes
-rw-r--r--app/assets/images/emoji/cinema.pngbin0 -> 585 bytes
-rw-r--r--app/assets/images/emoji/circus_tent.pngbin0 -> 1369 bytes
-rw-r--r--app/assets/images/emoji/city_dusk.pngbin0 -> 431 bytes
-rw-r--r--app/assets/images/emoji/city_sunset.pngbin0 -> 997 bytes
-rw-r--r--app/assets/images/emoji/cityscape.pngbin0 -> 599 bytes
-rw-r--r--app/assets/images/emoji/cl.pngbin0 -> 393 bytes
-rw-r--r--app/assets/images/emoji/clap.pngbin0 -> 1456 bytes
-rw-r--r--app/assets/images/emoji/clap_tone1.pngbin0 -> 1458 bytes
-rw-r--r--app/assets/images/emoji/clap_tone2.pngbin0 -> 1458 bytes
-rw-r--r--app/assets/images/emoji/clap_tone3.pngbin0 -> 1458 bytes
-rw-r--r--app/assets/images/emoji/clap_tone4.pngbin0 -> 1458 bytes
-rw-r--r--app/assets/images/emoji/clap_tone5.pngbin0 -> 1444 bytes
-rw-r--r--app/assets/images/emoji/clapper.pngbin0 -> 1535 bytes
-rw-r--r--app/assets/images/emoji/classical_building.pngbin0 -> 1006 bytes
-rw-r--r--app/assets/images/emoji/clipboard.pngbin0 -> 1345 bytes
-rw-r--r--app/assets/images/emoji/clock.pngbin0 -> 592 bytes
-rw-r--r--app/assets/images/emoji/clock1.pngbin0 -> 586 bytes
-rw-r--r--app/assets/images/emoji/clock10.pngbin0 -> 593 bytes
-rw-r--r--app/assets/images/emoji/clock1030.pngbin0 -> 530 bytes
-rw-r--r--app/assets/images/emoji/clock11.pngbin0 -> 590 bytes
-rw-r--r--app/assets/images/emoji/clock1130.pngbin0 -> 583 bytes
-rw-r--r--app/assets/images/emoji/clock12.pngbin0 -> 480 bytes
-rw-r--r--app/assets/images/emoji/clock1230.pngbin0 -> 579 bytes
-rw-r--r--app/assets/images/emoji/clock130.pngbin0 -> 526 bytes
-rw-r--r--app/assets/images/emoji/clock2.pngbin0 -> 591 bytes
-rw-r--r--app/assets/images/emoji/clock230.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/clock3.pngbin0 -> 482 bytes
-rw-r--r--app/assets/images/emoji/clock330.pngbin0 -> 568 bytes
-rw-r--r--app/assets/images/emoji/clock4.pngbin0 -> 592 bytes
-rw-r--r--app/assets/images/emoji/clock430.pngbin0 -> 531 bytes
-rw-r--r--app/assets/images/emoji/clock5.pngbin0 -> 585 bytes
-rw-r--r--app/assets/images/emoji/clock530.pngbin0 -> 552 bytes
-rw-r--r--app/assets/images/emoji/clock6.pngbin0 -> 466 bytes
-rw-r--r--app/assets/images/emoji/clock630.pngbin0 -> 536 bytes
-rw-r--r--app/assets/images/emoji/clock7.pngbin0 -> 581 bytes
-rw-r--r--app/assets/images/emoji/clock730.pngbin0 -> 531 bytes
-rw-r--r--app/assets/images/emoji/clock8.pngbin0 -> 590 bytes
-rw-r--r--app/assets/images/emoji/clock830.pngbin0 -> 570 bytes
-rw-r--r--app/assets/images/emoji/clock9.pngbin0 -> 484 bytes
-rw-r--r--app/assets/images/emoji/clock930.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/closed_book.pngbin0 -> 1359 bytes
-rw-r--r--app/assets/images/emoji/closed_lock_with_key.pngbin0 -> 1250 bytes
-rw-r--r--app/assets/images/emoji/closed_umbrella.pngbin0 -> 1002 bytes
-rw-r--r--app/assets/images/emoji/cloud.pngbin0 -> 626 bytes
-rw-r--r--app/assets/images/emoji/cloud_lightning.pngbin0 -> 767 bytes
-rw-r--r--app/assets/images/emoji/cloud_rain.pngbin0 -> 876 bytes
-rw-r--r--app/assets/images/emoji/cloud_snow.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/cloud_tornado.pngbin0 -> 1519 bytes
-rw-r--r--app/assets/images/emoji/clown.pngbin0 -> 1818 bytes
-rw-r--r--app/assets/images/emoji/clubs.pngbin0 -> 458 bytes
-rw-r--r--app/assets/images/emoji/cocktail.pngbin0 -> 1027 bytes
-rw-r--r--app/assets/images/emoji/coffee.pngbin0 -> 1679 bytes
-rw-r--r--app/assets/images/emoji/coffin.pngbin0 -> 2195 bytes
-rw-r--r--app/assets/images/emoji/cold_sweat.pngbin0 -> 971 bytes
-rw-r--r--app/assets/images/emoji/comet.pngbin0 -> 1819 bytes
-rw-r--r--app/assets/images/emoji/compression.pngbin0 -> 1612 bytes
-rw-r--r--app/assets/images/emoji/computer.pngbin0 -> 369 bytes
-rw-r--r--app/assets/images/emoji/confetti_ball.pngbin0 -> 1703 bytes
-rw-r--r--app/assets/images/emoji/confounded.pngbin0 -> 844 bytes
-rw-r--r--app/assets/images/emoji/confused.pngbin0 -> 647 bytes
-rw-r--r--app/assets/images/emoji/congratulations.pngbin0 -> 729 bytes
-rw-r--r--app/assets/images/emoji/construction.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/construction_site.pngbin0 -> 668 bytes
-rw-r--r--app/assets/images/emoji/construction_worker.pngbin0 -> 1126 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone1.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone2.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone3.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone4.pngbin0 -> 1095 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone5.pngbin0 -> 1119 bytes
-rw-r--r--app/assets/images/emoji/control_knobs.pngbin0 -> 1104 bytes
-rw-r--r--app/assets/images/emoji/convenience_store.pngbin0 -> 528 bytes
-rw-r--r--app/assets/images/emoji/cookie.pngbin0 -> 1351 bytes
-rw-r--r--app/assets/images/emoji/cooking.pngbin0 -> 764 bytes
-rw-r--r--app/assets/images/emoji/cool.pngbin0 -> 396 bytes
-rw-r--r--app/assets/images/emoji/cop.pngbin0 -> 1440 bytes
-rw-r--r--app/assets/images/emoji/cop_tone1.pngbin0 -> 1421 bytes
-rw-r--r--app/assets/images/emoji/cop_tone2.pngbin0 -> 1424 bytes
-rw-r--r--app/assets/images/emoji/cop_tone3.pngbin0 -> 1419 bytes
-rw-r--r--app/assets/images/emoji/cop_tone4.pngbin0 -> 1417 bytes
-rw-r--r--app/assets/images/emoji/cop_tone5.pngbin0 -> 1433 bytes
-rw-r--r--app/assets/images/emoji/copyright.pngbin0 -> 530 bytes
-rw-r--r--app/assets/images/emoji/corn.pngbin0 -> 1547 bytes
-rw-r--r--app/assets/images/emoji/couch.pngbin0 -> 1362 bytes
-rw-r--r--app/assets/images/emoji/couple.pngbin0 -> 1537 bytes
-rw-r--r--app/assets/images/emoji/couple_mm.pngbin0 -> 1091 bytes
-rw-r--r--app/assets/images/emoji/couple_with_heart.pngbin0 -> 1285 bytes
-rw-r--r--app/assets/images/emoji/couple_ww.pngbin0 -> 1034 bytes
-rw-r--r--app/assets/images/emoji/couplekiss.pngbin0 -> 1380 bytes
-rw-r--r--app/assets/images/emoji/cow.pngbin0 -> 1640 bytes
-rw-r--r--app/assets/images/emoji/cow2.pngbin0 -> 1810 bytes
-rw-r--r--app/assets/images/emoji/cowboy.pngbin0 -> 1353 bytes
-rw-r--r--app/assets/images/emoji/crab.pngbin0 -> 1475 bytes
-rw-r--r--app/assets/images/emoji/crayon.pngbin0 -> 633 bytes
-rw-r--r--app/assets/images/emoji/credit_card.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/crescent_moon.pngbin0 -> 446 bytes
-rw-r--r--app/assets/images/emoji/cricket.pngbin0 -> 1060 bytes
-rw-r--r--app/assets/images/emoji/crocodile.pngbin0 -> 2408 bytes
-rw-r--r--app/assets/images/emoji/croissant.pngbin0 -> 1313 bytes
-rw-r--r--app/assets/images/emoji/cross.pngbin0 -> 408 bytes
-rw-r--r--app/assets/images/emoji/crossed_flags.pngbin0 -> 1239 bytes
-rw-r--r--app/assets/images/emoji/crossed_swords.pngbin0 -> 1591 bytes
-rw-r--r--app/assets/images/emoji/crown.pngbin0 -> 1534 bytes
-rw-r--r--app/assets/images/emoji/cruise_ship.pngbin0 -> 2272 bytes
-rw-r--r--app/assets/images/emoji/cry.pngbin0 -> 1123 bytes
-rw-r--r--app/assets/images/emoji/crying_cat_face.pngbin0 -> 1875 bytes
-rw-r--r--app/assets/images/emoji/crystal_ball.pngbin0 -> 1913 bytes
-rw-r--r--app/assets/images/emoji/cucumber.pngbin0 -> 1357 bytes
-rw-r--r--app/assets/images/emoji/cupid.pngbin0 -> 846 bytes
-rw-r--r--app/assets/images/emoji/curly_loop.pngbin0 -> 545 bytes
-rw-r--r--app/assets/images/emoji/currency_exchange.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/curry.pngbin0 -> 1754 bytes
-rw-r--r--app/assets/images/emoji/custard.pngbin0 -> 1273 bytes
-rw-r--r--app/assets/images/emoji/customs.pngbin0 -> 648 bytes
-rw-r--r--app/assets/images/emoji/cyclone.pngbin0 -> 797 bytes
-rw-r--r--app/assets/images/emoji/dagger.pngbin0 -> 916 bytes
-rw-r--r--app/assets/images/emoji/dancer.pngbin0 -> 1405 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone1.pngbin0 -> 1420 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone2.pngbin0 -> 1423 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone3.pngbin0 -> 1429 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone4.pngbin0 -> 1428 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone5.pngbin0 -> 1418 bytes
-rw-r--r--app/assets/images/emoji/dancers.pngbin0 -> 1872 bytes
-rw-r--r--app/assets/images/emoji/dango.pngbin0 -> 802 bytes
-rw-r--r--app/assets/images/emoji/dark_sunglasses.pngbin0 -> 829 bytes
-rw-r--r--app/assets/images/emoji/dart.pngbin0 -> 1374 bytes
-rw-r--r--app/assets/images/emoji/dash.pngbin0 -> 840 bytes
-rw-r--r--app/assets/images/emoji/date.pngbin0 -> 788 bytes
-rw-r--r--app/assets/images/emoji/deciduous_tree.pngbin0 -> 1267 bytes
-rw-r--r--app/assets/images/emoji/deer.pngbin0 -> 1606 bytes
-rw-r--r--app/assets/images/emoji/department_store.pngbin0 -> 673 bytes
-rw-r--r--app/assets/images/emoji/desert.pngbin0 -> 1443 bytes
-rw-r--r--app/assets/images/emoji/desktop.pngbin0 -> 311 bytes
-rw-r--r--app/assets/images/emoji/diamond_shape_with_a_dot_inside.pngbin0 -> 693 bytes
-rw-r--r--app/assets/images/emoji/diamonds.pngbin0 -> 247 bytes
-rw-r--r--app/assets/images/emoji/disappointed.pngbin0 -> 757 bytes
-rw-r--r--app/assets/images/emoji/disappointed_relieved.pngbin0 -> 835 bytes
-rw-r--r--app/assets/images/emoji/dividers.pngbin0 -> 810 bytes
-rw-r--r--app/assets/images/emoji/dizzy.pngbin0 -> 795 bytes
-rw-r--r--app/assets/images/emoji/dizzy_face.pngbin0 -> 710 bytes
-rw-r--r--app/assets/images/emoji/do_not_litter.pngbin0 -> 1010 bytes
-rw-r--r--app/assets/images/emoji/dog.pngbin0 -> 1674 bytes
-rw-r--r--app/assets/images/emoji/dog2.pngbin0 -> 2085 bytes
-rw-r--r--app/assets/images/emoji/dollar.pngbin0 -> 405 bytes
-rw-r--r--app/assets/images/emoji/dolls.pngbin0 -> 2249 bytes
-rw-r--r--app/assets/images/emoji/dolphin.pngbin0 -> 1697 bytes
-rw-r--r--app/assets/images/emoji/door.pngbin0 -> 1105 bytes
-rw-r--r--app/assets/images/emoji/doughnut.pngbin0 -> 1322 bytes
-rw-r--r--app/assets/images/emoji/dove.pngbin0 -> 967 bytes
-rw-r--r--app/assets/images/emoji/dragon.pngbin0 -> 1574 bytes
-rw-r--r--app/assets/images/emoji/dragon_face.pngbin0 -> 1769 bytes
-rw-r--r--app/assets/images/emoji/dress.pngbin0 -> 1001 bytes
-rw-r--r--app/assets/images/emoji/dromedary_camel.pngbin0 -> 1515 bytes
-rw-r--r--app/assets/images/emoji/drooling_face.pngbin0 -> 1049 bytes
-rw-r--r--app/assets/images/emoji/droplet.pngbin0 -> 411 bytes
-rw-r--r--app/assets/images/emoji/drum.pngbin0 -> 1870 bytes
-rw-r--r--app/assets/images/emoji/duck.pngbin0 -> 1729 bytes
-rw-r--r--app/assets/images/emoji/dvd.pngbin0 -> 933 bytes
-rw-r--r--app/assets/images/emoji/e-mail.pngbin0 -> 1196 bytes
-rw-r--r--app/assets/images/emoji/eagle.pngbin0 -> 2222 bytes
-rw-r--r--app/assets/images/emoji/ear.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/ear_of_rice.pngbin0 -> 1422 bytes
-rw-r--r--app/assets/images/emoji/ear_tone1.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/ear_tone2.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/ear_tone3.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/ear_tone4.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/ear_tone5.pngbin0 -> 860 bytes
-rw-r--r--app/assets/images/emoji/earth_africa.pngbin0 -> 978 bytes
-rw-r--r--app/assets/images/emoji/earth_americas.pngbin0 -> 1031 bytes
-rw-r--r--app/assets/images/emoji/earth_asia.pngbin0 -> 966 bytes
-rw-r--r--app/assets/images/emoji/egg.pngbin0 -> 710 bytes
-rw-r--r--app/assets/images/emoji/eggplant.pngbin0 -> 773 bytes
-rw-r--r--app/assets/images/emoji/eight.pngbin0 -> 608 bytes
-rw-r--r--app/assets/images/emoji/eight_pointed_black_star.pngbin0 -> 493 bytes
-rw-r--r--app/assets/images/emoji/eight_spoked_asterisk.pngbin0 -> 493 bytes
-rw-r--r--app/assets/images/emoji/eject.pngbin0 -> 548 bytes
-rw-r--r--app/assets/images/emoji/electric_plug.pngbin0 -> 548 bytes
-rw-r--r--app/assets/images/emoji/elephant.pngbin0 -> 1293 bytes
-rw-r--r--app/assets/images/emoji/end.pngbin0 -> 393 bytes
-rw-r--r--app/assets/images/emoji/envelope.pngbin0 -> 916 bytes
-rw-r--r--app/assets/images/emoji/envelope_with_arrow.pngbin0 -> 1062 bytes
-rw-r--r--app/assets/images/emoji/euro.pngbin0 -> 460 bytes
-rw-r--r--app/assets/images/emoji/european_castle.pngbin0 -> 965 bytes
-rw-r--r--app/assets/images/emoji/european_post_office.pngbin0 -> 551 bytes
-rw-r--r--app/assets/images/emoji/evergreen_tree.pngbin0 -> 719 bytes
-rw-r--r--app/assets/images/emoji/exclamation.pngbin0 -> 354 bytes
-rw-r--r--app/assets/images/emoji/expressionless.pngbin0 -> 438 bytes
-rw-r--r--app/assets/images/emoji/eye.pngbin0 -> 664 bytes
-rw-r--r--app/assets/images/emoji/eye_in_speech_bubble.pngbin0 -> 698 bytes
-rw-r--r--app/assets/images/emoji/eyeglasses.pngbin0 -> 577 bytes
-rw-r--r--app/assets/images/emoji/eyes.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/face_palm.pngbin0 -> 1523 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone1.pngbin0 -> 1563 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone2.pngbin0 -> 1547 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone3.pngbin0 -> 1550 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone4.pngbin0 -> 1553 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone5.pngbin0 -> 1532 bytes
-rw-r--r--app/assets/images/emoji/factory.pngbin0 -> 936 bytes
-rw-r--r--app/assets/images/emoji/fallen_leaf.pngbin0 -> 951 bytes
-rw-r--r--app/assets/images/emoji/family.pngbin0 -> 1433 bytes
-rw-r--r--app/assets/images/emoji/family_mmb.pngbin0 -> 1206 bytes
-rw-r--r--app/assets/images/emoji/family_mmbb.pngbin0 -> 1349 bytes
-rw-r--r--app/assets/images/emoji/family_mmg.pngbin0 -> 1361 bytes
-rw-r--r--app/assets/images/emoji/family_mmgb.pngbin0 -> 1626 bytes
-rw-r--r--app/assets/images/emoji/family_mmgg.pngbin0 -> 1448 bytes
-rw-r--r--app/assets/images/emoji/family_mwbb.pngbin0 -> 1638 bytes
-rw-r--r--app/assets/images/emoji/family_mwg.pngbin0 -> 1554 bytes
-rw-r--r--app/assets/images/emoji/family_mwgb.pngbin0 -> 1837 bytes
-rw-r--r--app/assets/images/emoji/family_mwgg.pngbin0 -> 1738 bytes
-rw-r--r--app/assets/images/emoji/family_wwb.pngbin0 -> 1155 bytes
-rw-r--r--app/assets/images/emoji/family_wwbb.pngbin0 -> 1289 bytes
-rw-r--r--app/assets/images/emoji/family_wwg.pngbin0 -> 1286 bytes
-rw-r--r--app/assets/images/emoji/family_wwgb.pngbin0 -> 1550 bytes
-rw-r--r--app/assets/images/emoji/family_wwgg.pngbin0 -> 1374 bytes
-rw-r--r--app/assets/images/emoji/fast_forward.pngbin0 -> 523 bytes
-rw-r--r--app/assets/images/emoji/fax.pngbin0 -> 1188 bytes
-rw-r--r--app/assets/images/emoji/fearful.pngbin0 -> 1002 bytes
-rw-r--r--app/assets/images/emoji/feet.pngbin0 -> 603 bytes
-rw-r--r--app/assets/images/emoji/fencer.pngbin0 -> 1342 bytes
-rw-r--r--app/assets/images/emoji/ferris_wheel.pngbin0 -> 2185 bytes
-rw-r--r--app/assets/images/emoji/ferry.pngbin0 -> 528 bytes
-rw-r--r--app/assets/images/emoji/field_hockey.pngbin0 -> 947 bytes
-rw-r--r--app/assets/images/emoji/file_cabinet.pngbin0 -> 1420 bytes
-rw-r--r--app/assets/images/emoji/file_folder.pngbin0 -> 1445 bytes
-rw-r--r--app/assets/images/emoji/film_frames.pngbin0 -> 560 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed.pngbin0 -> 1050 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone1.pngbin0 -> 1047 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone2.pngbin0 -> 1050 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone3.pngbin0 -> 1050 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone4.pngbin0 -> 1046 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone5.pngbin0 -> 1050 bytes
-rw-r--r--app/assets/images/emoji/fire.pngbin0 -> 1020 bytes
-rw-r--r--app/assets/images/emoji/fire_engine.pngbin0 -> 1656 bytes
-rw-r--r--app/assets/images/emoji/fireworks.pngbin0 -> 1364 bytes
-rw-r--r--app/assets/images/emoji/first_place.pngbin0 -> 1419 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon.pngbin0 -> 1152 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon_with_face.pngbin0 -> 1068 bytes
-rw-r--r--app/assets/images/emoji/fish.pngbin0 -> 1080 bytes
-rw-r--r--app/assets/images/emoji/fish_cake.pngbin0 -> 1245 bytes
-rw-r--r--app/assets/images/emoji/fishing_pole_and_fish.pngbin0 -> 1442 bytes
-rw-r--r--app/assets/images/emoji/fist.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/fist_tone1.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/fist_tone2.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/fist_tone3.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/fist_tone4.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/fist_tone5.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/five.pngbin0 -> 577 bytes
-rw-r--r--app/assets/images/emoji/flag_ac.pngbin0 -> 1934 bytes
-rw-r--r--app/assets/images/emoji/flag_ad.pngbin0 -> 1285 bytes
-rw-r--r--app/assets/images/emoji/flag_ae.pngbin0 -> 544 bytes
-rw-r--r--app/assets/images/emoji/flag_af.pngbin0 -> 942 bytes
-rw-r--r--app/assets/images/emoji/flag_ag.pngbin0 -> 913 bytes
-rw-r--r--app/assets/images/emoji/flag_ai.pngbin0 -> 1056 bytes
-rw-r--r--app/assets/images/emoji/flag_al.pngbin0 -> 905 bytes
-rw-r--r--app/assets/images/emoji/flag_am.pngbin0 -> 514 bytes
-rw-r--r--app/assets/images/emoji/flag_ao.pngbin0 -> 997 bytes
-rw-r--r--app/assets/images/emoji/flag_aq.pngbin0 -> 657 bytes
-rw-r--r--app/assets/images/emoji/flag_ar.pngbin0 -> 975 bytes
-rw-r--r--app/assets/images/emoji/flag_as.pngbin0 -> 1489 bytes
-rw-r--r--app/assets/images/emoji/flag_at.pngbin0 -> 430 bytes
-rw-r--r--app/assets/images/emoji/flag_au.pngbin0 -> 962 bytes
-rw-r--r--app/assets/images/emoji/flag_aw.pngbin0 -> 709 bytes
-rw-r--r--app/assets/images/emoji/flag_ax.pngbin0 -> 496 bytes
-rw-r--r--app/assets/images/emoji/flag_az.pngbin0 -> 709 bytes
-rw-r--r--app/assets/images/emoji/flag_ba.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/flag_bb.pngbin0 -> 789 bytes
-rw-r--r--app/assets/images/emoji/flag_bd.pngbin0 -> 490 bytes
-rw-r--r--app/assets/images/emoji/flag_be.pngbin0 -> 444 bytes
-rw-r--r--app/assets/images/emoji/flag_bf.pngbin0 -> 717 bytes
-rw-r--r--app/assets/images/emoji/flag_bg.pngbin0 -> 513 bytes
-rw-r--r--app/assets/images/emoji/flag_bh.pngbin0 -> 593 bytes
-rw-r--r--app/assets/images/emoji/flag_bi.pngbin0 -> 795 bytes
-rw-r--r--app/assets/images/emoji/flag_bj.pngbin0 -> 554 bytes
-rw-r--r--app/assets/images/emoji/flag_bl.pngbin0 -> 1691 bytes
-rw-r--r--app/assets/images/emoji/flag_black.pngbin0 -> 702 bytes
-rw-r--r--app/assets/images/emoji/flag_bm.pngbin0 -> 1374 bytes
-rw-r--r--app/assets/images/emoji/flag_bn.pngbin0 -> 1355 bytes
-rw-r--r--app/assets/images/emoji/flag_bo.pngbin0 -> 1132 bytes
-rw-r--r--app/assets/images/emoji/flag_bq.pngbin0 -> 1144 bytes
-rw-r--r--app/assets/images/emoji/flag_br.pngbin0 -> 819 bytes
-rw-r--r--app/assets/images/emoji/flag_bs.pngbin0 -> 448 bytes
-rw-r--r--app/assets/images/emoji/flag_bt.pngbin0 -> 1213 bytes
-rw-r--r--app/assets/images/emoji/flag_bv.pngbin0 -> 495 bytes
-rw-r--r--app/assets/images/emoji/flag_bw.pngbin0 -> 391 bytes
-rw-r--r--app/assets/images/emoji/flag_by.pngbin0 -> 1120 bytes
-rw-r--r--app/assets/images/emoji/flag_bz.pngbin0 -> 1595 bytes
-rw-r--r--app/assets/images/emoji/flag_ca.pngbin0 -> 755 bytes
-rw-r--r--app/assets/images/emoji/flag_cc.pngbin0 -> 851 bytes
-rw-r--r--app/assets/images/emoji/flag_cd.pngbin0 -> 707 bytes
-rw-r--r--app/assets/images/emoji/flag_cf.pngbin0 -> 673 bytes
-rw-r--r--app/assets/images/emoji/flag_cg.pngbin0 -> 586 bytes
-rw-r--r--app/assets/images/emoji/flag_ch.pngbin0 -> 390 bytes
-rw-r--r--app/assets/images/emoji/flag_ci.pngbin0 -> 440 bytes
-rw-r--r--app/assets/images/emoji/flag_ck.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/flag_cl.pngbin0 -> 748 bytes
-rw-r--r--app/assets/images/emoji/flag_cm.pngbin0 -> 627 bytes
-rw-r--r--app/assets/images/emoji/flag_cn.pngbin0 -> 676 bytes
-rw-r--r--app/assets/images/emoji/flag_co.pngbin0 -> 524 bytes
-rw-r--r--app/assets/images/emoji/flag_cp.pngbin0 -> 443 bytes
-rw-r--r--app/assets/images/emoji/flag_cr.pngbin0 -> 419 bytes
-rw-r--r--app/assets/images/emoji/flag_cu.pngbin0 -> 586 bytes
-rw-r--r--app/assets/images/emoji/flag_cv.pngbin0 -> 642 bytes
-rw-r--r--app/assets/images/emoji/flag_cw.pngbin0 -> 665 bytes
-rw-r--r--app/assets/images/emoji/flag_cx.pngbin0 -> 1142 bytes
-rw-r--r--app/assets/images/emoji/flag_cy.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/emoji/flag_cz.pngbin0 -> 600 bytes
-rw-r--r--app/assets/images/emoji/flag_de.pngbin0 -> 502 bytes
-rw-r--r--app/assets/images/emoji/flag_dg.pngbin0 -> 1911 bytes
-rw-r--r--app/assets/images/emoji/flag_dj.pngbin0 -> 753 bytes
-rw-r--r--app/assets/images/emoji/flag_dk.pngbin0 -> 450 bytes
-rw-r--r--app/assets/images/emoji/flag_dm.pngbin0 -> 1075 bytes
-rw-r--r--app/assets/images/emoji/flag_do.pngbin0 -> 1135 bytes
-rw-r--r--app/assets/images/emoji/flag_dz.pngbin0 -> 734 bytes
-rw-r--r--app/assets/images/emoji/flag_ea.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/emoji/flag_ec.pngbin0 -> 1431 bytes
-rw-r--r--app/assets/images/emoji/flag_ee.pngbin0 -> 512 bytes
-rw-r--r--app/assets/images/emoji/flag_eg.pngbin0 -> 818 bytes
-rw-r--r--app/assets/images/emoji/flag_eh.pngbin0 -> 742 bytes
-rw-r--r--app/assets/images/emoji/flag_er.pngbin0 -> 1218 bytes
-rw-r--r--app/assets/images/emoji/flag_es.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/emoji/flag_et.pngbin0 -> 947 bytes
-rw-r--r--app/assets/images/emoji/flag_eu.pngbin0 -> 760 bytes
-rw-r--r--app/assets/images/emoji/flag_fi.pngbin0 -> 487 bytes
-rw-r--r--app/assets/images/emoji/flag_fj.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/flag_fk.pngbin0 -> 1558 bytes
-rw-r--r--app/assets/images/emoji/flag_fm.pngbin0 -> 554 bytes
-rw-r--r--app/assets/images/emoji/flag_fo.pngbin0 -> 495 bytes
-rw-r--r--app/assets/images/emoji/flag_fr.pngbin0 -> 443 bytes
-rw-r--r--app/assets/images/emoji/flag_ga.pngbin0 -> 512 bytes
-rw-r--r--app/assets/images/emoji/flag_gb.pngbin0 -> 919 bytes
-rw-r--r--app/assets/images/emoji/flag_gd.pngbin0 -> 1017 bytes
-rw-r--r--app/assets/images/emoji/flag_ge.pngbin0 -> 583 bytes
-rw-r--r--app/assets/images/emoji/flag_gf.pngbin0 -> 865 bytes
-rw-r--r--app/assets/images/emoji/flag_gg.pngbin0 -> 521 bytes
-rw-r--r--app/assets/images/emoji/flag_gh.pngbin0 -> 723 bytes
-rw-r--r--app/assets/images/emoji/flag_gi.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/flag_gl.pngbin0 -> 700 bytes
-rw-r--r--app/assets/images/emoji/flag_gm.pngbin0 -> 501 bytes
-rw-r--r--app/assets/images/emoji/flag_gn.pngbin0 -> 434 bytes
-rw-r--r--app/assets/images/emoji/flag_gp.pngbin0 -> 1587 bytes
-rw-r--r--app/assets/images/emoji/flag_gq.pngbin0 -> 1132 bytes
-rw-r--r--app/assets/images/emoji/flag_gr.pngbin0 -> 549 bytes
-rw-r--r--app/assets/images/emoji/flag_gs.pngbin0 -> 2115 bytes
-rw-r--r--app/assets/images/emoji/flag_gt.pngbin0 -> 1087 bytes
-rw-r--r--app/assets/images/emoji/flag_gu.pngbin0 -> 1045 bytes
-rw-r--r--app/assets/images/emoji/flag_gw.pngbin0 -> 705 bytes
-rw-r--r--app/assets/images/emoji/flag_gy.pngbin0 -> 690 bytes
-rw-r--r--app/assets/images/emoji/flag_hk.pngbin0 -> 759 bytes
-rw-r--r--app/assets/images/emoji/flag_hm.pngbin0 -> 1036 bytes
-rw-r--r--app/assets/images/emoji/flag_hn.pngbin0 -> 513 bytes
-rw-r--r--app/assets/images/emoji/flag_hr.pngbin0 -> 1411 bytes
-rw-r--r--app/assets/images/emoji/flag_ht.pngbin0 -> 1205 bytes
-rw-r--r--app/assets/images/emoji/flag_hu.pngbin0 -> 513 bytes
-rw-r--r--app/assets/images/emoji/flag_ic.pngbin0 -> 1330 bytes
-rw-r--r--app/assets/images/emoji/flag_id.pngbin0 -> 498 bytes
-rw-r--r--app/assets/images/emoji/flag_ie.pngbin0 -> 478 bytes
-rw-r--r--app/assets/images/emoji/flag_il.pngbin0 -> 658 bytes
-rw-r--r--app/assets/images/emoji/flag_im.pngbin0 -> 976 bytes
-rw-r--r--app/assets/images/emoji/flag_in.pngbin0 -> 773 bytes
-rw-r--r--app/assets/images/emoji/flag_io.pngbin0 -> 1911 bytes
-rw-r--r--app/assets/images/emoji/flag_iq.pngbin0 -> 811 bytes
-rw-r--r--app/assets/images/emoji/flag_ir.pngbin0 -> 1036 bytes
-rw-r--r--app/assets/images/emoji/flag_is.pngbin0 -> 491 bytes
-rw-r--r--app/assets/images/emoji/flag_it.pngbin0 -> 472 bytes
-rw-r--r--app/assets/images/emoji/flag_je.pngbin0 -> 956 bytes
-rw-r--r--app/assets/images/emoji/flag_jm.pngbin0 -> 837 bytes
-rw-r--r--app/assets/images/emoji/flag_jo.pngbin0 -> 740 bytes
-rw-r--r--app/assets/images/emoji/flag_jp.pngbin0 -> 455 bytes
-rw-r--r--app/assets/images/emoji/flag_ke.pngbin0 -> 1160 bytes
-rw-r--r--app/assets/images/emoji/flag_kg.pngbin0 -> 1080 bytes
-rw-r--r--app/assets/images/emoji/flag_kh.pngbin0 -> 872 bytes
-rw-r--r--app/assets/images/emoji/flag_ki.pngbin0 -> 1369 bytes
-rw-r--r--app/assets/images/emoji/flag_km.pngbin0 -> 783 bytes
-rw-r--r--app/assets/images/emoji/flag_kn.pngbin0 -> 1316 bytes
-rw-r--r--app/assets/images/emoji/flag_kp.pngbin0 -> 696 bytes
-rw-r--r--app/assets/images/emoji/flag_kr.pngbin0 -> 967 bytes
-rw-r--r--app/assets/images/emoji/flag_kw.pngbin0 -> 560 bytes
-rw-r--r--app/assets/images/emoji/flag_ky.pngbin0 -> 1671 bytes
-rw-r--r--app/assets/images/emoji/flag_kz.pngbin0 -> 1136 bytes
-rw-r--r--app/assets/images/emoji/flag_la.pngbin0 -> 479 bytes
-rw-r--r--app/assets/images/emoji/flag_lb.pngbin0 -> 740 bytes
-rw-r--r--app/assets/images/emoji/flag_lc.pngbin0 -> 561 bytes
-rw-r--r--app/assets/images/emoji/flag_li.pngbin0 -> 946 bytes
-rw-r--r--app/assets/images/emoji/flag_lk.pngbin0 -> 974 bytes
-rw-r--r--app/assets/images/emoji/flag_lr.pngbin0 -> 772 bytes
-rw-r--r--app/assets/images/emoji/flag_ls.pngbin0 -> 775 bytes
-rw-r--r--app/assets/images/emoji/flag_lt.pngbin0 -> 510 bytes
-rw-r--r--app/assets/images/emoji/flag_lu.pngbin0 -> 512 bytes
-rw-r--r--app/assets/images/emoji/flag_lv.pngbin0 -> 388 bytes
-rw-r--r--app/assets/images/emoji/flag_ly.pngbin0 -> 685 bytes
-rw-r--r--app/assets/images/emoji/flag_ma.pngbin0 -> 626 bytes
-rw-r--r--app/assets/images/emoji/flag_mc.pngbin0 -> 528 bytes
-rw-r--r--app/assets/images/emoji/flag_md.pngbin0 -> 1170 bytes
-rw-r--r--app/assets/images/emoji/flag_me.pngbin0 -> 1074 bytes
-rw-r--r--app/assets/images/emoji/flag_mf.pngbin0 -> 443 bytes
-rw-r--r--app/assets/images/emoji/flag_mg.pngbin0 -> 556 bytes
-rw-r--r--app/assets/images/emoji/flag_mh.pngbin0 -> 1138 bytes
-rw-r--r--app/assets/images/emoji/flag_mk.pngbin0 -> 1023 bytes
-rw-r--r--app/assets/images/emoji/flag_ml.pngbin0 -> 440 bytes
-rw-r--r--app/assets/images/emoji/flag_mm.pngbin0 -> 937 bytes
-rw-r--r--app/assets/images/emoji/flag_mn.pngbin0 -> 698 bytes
-rw-r--r--app/assets/images/emoji/flag_mo.pngbin0 -> 792 bytes
-rw-r--r--app/assets/images/emoji/flag_mp.pngbin0 -> 1797 bytes
-rw-r--r--app/assets/images/emoji/flag_mq.pngbin0 -> 780 bytes
-rw-r--r--app/assets/images/emoji/flag_mr.pngbin0 -> 657 bytes
-rw-r--r--app/assets/images/emoji/flag_ms.pngbin0 -> 1477 bytes
-rw-r--r--app/assets/images/emoji/flag_mt.pngbin0 -> 799 bytes
-rw-r--r--app/assets/images/emoji/flag_mu.pngbin0 -> 544 bytes
-rw-r--r--app/assets/images/emoji/flag_mv.pngbin0 -> 598 bytes
-rw-r--r--app/assets/images/emoji/flag_mw.pngbin0 -> 825 bytes
-rw-r--r--app/assets/images/emoji/flag_mx.pngbin0 -> 951 bytes
-rw-r--r--app/assets/images/emoji/flag_my.pngbin0 -> 775 bytes
-rw-r--r--app/assets/images/emoji/flag_mz.pngbin0 -> 1159 bytes
-rw-r--r--app/assets/images/emoji/flag_na.pngbin0 -> 1249 bytes
-rw-r--r--app/assets/images/emoji/flag_nc.pngbin0 -> 1148 bytes
-rw-r--r--app/assets/images/emoji/flag_ne.pngbin0 -> 593 bytes
-rw-r--r--app/assets/images/emoji/flag_nf.pngbin0 -> 877 bytes
-rw-r--r--app/assets/images/emoji/flag_ng.pngbin0 -> 438 bytes
-rw-r--r--app/assets/images/emoji/flag_ni.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/flag_nl.pngbin0 -> 499 bytes
-rw-r--r--app/assets/images/emoji/flag_no.pngbin0 -> 484 bytes
-rw-r--r--app/assets/images/emoji/flag_np.pngbin0 -> 802 bytes
-rw-r--r--app/assets/images/emoji/flag_nr.pngbin0 -> 529 bytes
-rw-r--r--app/assets/images/emoji/flag_nu.pngbin0 -> 1128 bytes
-rw-r--r--app/assets/images/emoji/flag_nz.pngbin0 -> 1099 bytes
-rw-r--r--app/assets/images/emoji/flag_om.pngbin0 -> 754 bytes
-rw-r--r--app/assets/images/emoji/flag_pa.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/emoji/flag_pe.pngbin0 -> 439 bytes
-rw-r--r--app/assets/images/emoji/flag_pf.pngbin0 -> 1091 bytes
-rw-r--r--app/assets/images/emoji/flag_pg.pngbin0 -> 1076 bytes
-rw-r--r--app/assets/images/emoji/flag_ph.pngbin0 -> 867 bytes
-rw-r--r--app/assets/images/emoji/flag_pk.pngbin0 -> 753 bytes
-rw-r--r--app/assets/images/emoji/flag_pl.pngbin0 -> 522 bytes
-rw-r--r--app/assets/images/emoji/flag_pm.pngbin0 -> 2314 bytes
-rw-r--r--app/assets/images/emoji/flag_pn.pngbin0 -> 1895 bytes
-rw-r--r--app/assets/images/emoji/flag_pr.pngbin0 -> 605 bytes
-rw-r--r--app/assets/images/emoji/flag_ps.pngbin0 -> 574 bytes
-rw-r--r--app/assets/images/emoji/flag_pt.pngbin0 -> 1055 bytes
-rw-r--r--app/assets/images/emoji/flag_pw.pngbin0 -> 475 bytes
-rw-r--r--app/assets/images/emoji/flag_py.pngbin0 -> 1085 bytes
-rw-r--r--app/assets/images/emoji/flag_qa.pngbin0 -> 657 bytes
-rw-r--r--app/assets/images/emoji/flag_re.pngbin0 -> 837 bytes
-rw-r--r--app/assets/images/emoji/flag_ro.pngbin0 -> 441 bytes
-rw-r--r--app/assets/images/emoji/flag_rs.pngbin0 -> 1237 bytes
-rw-r--r--app/assets/images/emoji/flag_ru.pngbin0 -> 496 bytes
-rw-r--r--app/assets/images/emoji/flag_rw.pngbin0 -> 940 bytes
-rw-r--r--app/assets/images/emoji/flag_sa.pngbin0 -> 781 bytes
-rw-r--r--app/assets/images/emoji/flag_sb.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/flag_sc.pngbin0 -> 1073 bytes
-rw-r--r--app/assets/images/emoji/flag_sd.pngbin0 -> 578 bytes
-rw-r--r--app/assets/images/emoji/flag_se.pngbin0 -> 455 bytes
-rw-r--r--app/assets/images/emoji/flag_sg.pngbin0 -> 730 bytes
-rw-r--r--app/assets/images/emoji/flag_sh.pngbin0 -> 1369 bytes
-rw-r--r--app/assets/images/emoji/flag_si.pngbin0 -> 1030 bytes
-rw-r--r--app/assets/images/emoji/flag_sj.pngbin0 -> 495 bytes
-rw-r--r--app/assets/images/emoji/flag_sk.pngbin0 -> 780 bytes
-rw-r--r--app/assets/images/emoji/flag_sl.pngbin0 -> 510 bytes
-rw-r--r--app/assets/images/emoji/flag_sm.pngbin0 -> 2000 bytes
-rw-r--r--app/assets/images/emoji/flag_sn.pngbin0 -> 621 bytes
-rw-r--r--app/assets/images/emoji/flag_so.pngbin0 -> 609 bytes
-rw-r--r--app/assets/images/emoji/flag_sr.pngbin0 -> 650 bytes
-rw-r--r--app/assets/images/emoji/flag_ss.pngbin0 -> 722 bytes
-rw-r--r--app/assets/images/emoji/flag_st.pngbin0 -> 562 bytes
-rw-r--r--app/assets/images/emoji/flag_sv.pngbin0 -> 1125 bytes
-rw-r--r--app/assets/images/emoji/flag_sx.pngbin0 -> 1195 bytes
-rw-r--r--app/assets/images/emoji/flag_sy.pngbin0 -> 696 bytes
-rw-r--r--app/assets/images/emoji/flag_sz.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/flag_ta.pngbin0 -> 1907 bytes
-rw-r--r--app/assets/images/emoji/flag_tc.pngbin0 -> 1538 bytes
-rw-r--r--app/assets/images/emoji/flag_td.pngbin0 -> 443 bytes
-rw-r--r--app/assets/images/emoji/flag_tf.pngbin0 -> 857 bytes
-rw-r--r--app/assets/images/emoji/flag_tg.pngbin0 -> 790 bytes
-rw-r--r--app/assets/images/emoji/flag_th.pngbin0 -> 421 bytes
-rw-r--r--app/assets/images/emoji/flag_tj.pngbin0 -> 906 bytes
-rw-r--r--app/assets/images/emoji/flag_tk.pngbin0 -> 835 bytes
-rw-r--r--app/assets/images/emoji/flag_tl.pngbin0 -> 849 bytes
-rw-r--r--app/assets/images/emoji/flag_tm.pngbin0 -> 1178 bytes
-rw-r--r--app/assets/images/emoji/flag_tn.pngbin0 -> 625 bytes
-rw-r--r--app/assets/images/emoji/flag_to.pngbin0 -> 553 bytes
-rw-r--r--app/assets/images/emoji/flag_tr.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/flag_tt.pngbin0 -> 604 bytes
-rw-r--r--app/assets/images/emoji/flag_tv.pngbin0 -> 1120 bytes
-rw-r--r--app/assets/images/emoji/flag_tw.pngbin0 -> 761 bytes
-rw-r--r--app/assets/images/emoji/flag_tz.pngbin0 -> 1061 bytes
-rw-r--r--app/assets/images/emoji/flag_ua.pngbin0 -> 528 bytes
-rw-r--r--app/assets/images/emoji/flag_ug.pngbin0 -> 887 bytes
-rw-r--r--app/assets/images/emoji/flag_um.pngbin0 -> 776 bytes
-rw-r--r--app/assets/images/emoji/flag_us.pngbin0 -> 776 bytes
-rw-r--r--app/assets/images/emoji/flag_uy.pngbin0 -> 966 bytes
-rw-r--r--app/assets/images/emoji/flag_uz.pngbin0 -> 750 bytes
-rw-r--r--app/assets/images/emoji/flag_va.pngbin0 -> 1331 bytes
-rw-r--r--app/assets/images/emoji/flag_vc.pngbin0 -> 897 bytes
-rw-r--r--app/assets/images/emoji/flag_ve.pngbin0 -> 748 bytes
-rw-r--r--app/assets/images/emoji/flag_vg.pngbin0 -> 1789 bytes
-rw-r--r--app/assets/images/emoji/flag_vi.pngbin0 -> 1378 bytes
-rw-r--r--app/assets/images/emoji/flag_vn.pngbin0 -> 583 bytes
-rw-r--r--app/assets/images/emoji/flag_vu.pngbin0 -> 844 bytes
-rw-r--r--app/assets/images/emoji/flag_wf.pngbin0 -> 443 bytes
-rw-r--r--app/assets/images/emoji/flag_white.pngbin0 -> 699 bytes
-rw-r--r--app/assets/images/emoji/flag_ws.pngbin0 -> 634 bytes
-rw-r--r--app/assets/images/emoji/flag_xk.pngbin0 -> 722 bytes
-rw-r--r--app/assets/images/emoji/flag_ye.pngbin0 -> 507 bytes
-rw-r--r--app/assets/images/emoji/flag_yt.pngbin0 -> 1623 bytes
-rw-r--r--app/assets/images/emoji/flag_za.pngbin0 -> 676 bytes
-rw-r--r--app/assets/images/emoji/flag_zm.pngbin0 -> 881 bytes
-rw-r--r--app/assets/images/emoji/flag_zw.pngbin0 -> 993 bytes
-rw-r--r--app/assets/images/emoji/flags.pngbin0 -> 1722 bytes
-rw-r--r--app/assets/images/emoji/flashlight.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/fleur-de-lis.pngbin0 -> 632 bytes
-rw-r--r--app/assets/images/emoji/floppy_disk.pngbin0 -> 258 bytes
-rw-r--r--app/assets/images/emoji/flower_playing_cards.pngbin0 -> 449 bytes
-rw-r--r--app/assets/images/emoji/flushed.pngbin0 -> 1127 bytes
-rw-r--r--app/assets/images/emoji/fog.pngbin0 -> 713 bytes
-rw-r--r--app/assets/images/emoji/foggy.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/football.pngbin0 -> 956 bytes
-rw-r--r--app/assets/images/emoji/footprints.pngbin0 -> 621 bytes
-rw-r--r--app/assets/images/emoji/fork_and_knife.pngbin0 -> 668 bytes
-rw-r--r--app/assets/images/emoji/fork_knife_plate.pngbin0 -> 976 bytes
-rw-r--r--app/assets/images/emoji/fountain.pngbin0 -> 1768 bytes
-rw-r--r--app/assets/images/emoji/four.pngbin0 -> 497 bytes
-rw-r--r--app/assets/images/emoji/four_leaf_clover.pngbin0 -> 1156 bytes
-rw-r--r--app/assets/images/emoji/fox.pngbin0 -> 1556 bytes
-rw-r--r--app/assets/images/emoji/frame_photo.pngbin0 -> 514 bytes
-rw-r--r--app/assets/images/emoji/free.pngbin0 -> 370 bytes
-rw-r--r--app/assets/images/emoji/french_bread.pngbin0 -> 1551 bytes
-rw-r--r--app/assets/images/emoji/fried_shrimp.pngbin0 -> 1241 bytes
-rw-r--r--app/assets/images/emoji/fries.pngbin0 -> 1873 bytes
-rw-r--r--app/assets/images/emoji/frog.pngbin0 -> 897 bytes
-rw-r--r--app/assets/images/emoji/frowning.pngbin0 -> 633 bytes
-rw-r--r--app/assets/images/emoji/frowning2.pngbin0 -> 589 bytes
-rw-r--r--app/assets/images/emoji/fuelpump.pngbin0 -> 864 bytes
-rw-r--r--app/assets/images/emoji/full_moon.pngbin0 -> 841 bytes
-rw-r--r--app/assets/images/emoji/full_moon_with_face.pngbin0 -> 1186 bytes
-rw-r--r--app/assets/images/emoji/game_die.pngbin0 -> 1136 bytes
-rw-r--r--app/assets/images/emoji/gear.pngbin0 -> 747 bytes
-rw-r--r--app/assets/images/emoji/gem.pngbin0 -> 715 bytes
-rw-r--r--app/assets/images/emoji/gemini.pngbin0 -> 547 bytes
-rw-r--r--app/assets/images/emoji/ghost.pngbin0 -> 1465 bytes
-rw-r--r--app/assets/images/emoji/gift.pngbin0 -> 1966 bytes
-rw-r--r--app/assets/images/emoji/gift_heart.pngbin0 -> 1141 bytes
-rw-r--r--app/assets/images/emoji/girl.pngbin0 -> 1261 bytes
-rw-r--r--app/assets/images/emoji/girl_tone1.pngbin0 -> 1259 bytes
-rw-r--r--app/assets/images/emoji/girl_tone2.pngbin0 -> 1255 bytes
-rw-r--r--app/assets/images/emoji/girl_tone3.pngbin0 -> 1255 bytes
-rw-r--r--app/assets/images/emoji/girl_tone4.pngbin0 -> 1241 bytes
-rw-r--r--app/assets/images/emoji/girl_tone5.pngbin0 -> 1245 bytes
-rw-r--r--app/assets/images/emoji/globe_with_meridians.pngbin0 -> 796 bytes
-rw-r--r--app/assets/images/emoji/goal.pngbin0 -> 1242 bytes
-rw-r--r--app/assets/images/emoji/goat.pngbin0 -> 981 bytes
-rw-r--r--app/assets/images/emoji/golf.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/golfer.pngbin0 -> 1189 bytes
-rw-r--r--app/assets/images/emoji/gorilla.pngbin0 -> 1090 bytes
-rw-r--r--app/assets/images/emoji/grapes.pngbin0 -> 1552 bytes
-rw-r--r--app/assets/images/emoji/green_apple.pngbin0 -> 656 bytes
-rw-r--r--app/assets/images/emoji/green_book.pngbin0 -> 1366 bytes
-rw-r--r--app/assets/images/emoji/green_heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/grey_exclamation.pngbin0 -> 354 bytes
-rw-r--r--app/assets/images/emoji/grey_question.pngbin0 -> 449 bytes
-rw-r--r--app/assets/images/emoji/grimacing.pngbin0 -> 694 bytes
-rw-r--r--app/assets/images/emoji/grin.pngbin0 -> 767 bytes
-rw-r--r--app/assets/images/emoji/grinning.pngbin0 -> 810 bytes
-rw-r--r--app/assets/images/emoji/guardsman.pngbin0 -> 1140 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone1.pngbin0 -> 1122 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone2.pngbin0 -> 1160 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone3.pngbin0 -> 1160 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone4.pngbin0 -> 1157 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone5.pngbin0 -> 1165 bytes
-rw-r--r--app/assets/images/emoji/guitar.pngbin0 -> 1056 bytes
-rw-r--r--app/assets/images/emoji/gun.pngbin0 -> 1859 bytes
-rw-r--r--app/assets/images/emoji/haircut.pngbin0 -> 1935 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone1.pngbin0 -> 1945 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone2.pngbin0 -> 1935 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone3.pngbin0 -> 1923 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone4.pngbin0 -> 1904 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone5.pngbin0 -> 1920 bytes
-rw-r--r--app/assets/images/emoji/hamburger.pngbin0 -> 1973 bytes
-rw-r--r--app/assets/images/emoji/hammer.pngbin0 -> 834 bytes
-rw-r--r--app/assets/images/emoji/hammer_pick.pngbin0 -> 1068 bytes
-rw-r--r--app/assets/images/emoji/hamster.pngbin0 -> 1279 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone1.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone2.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone3.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone4.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone5.pngbin0 -> 1081 bytes
-rw-r--r--app/assets/images/emoji/handbag.pngbin0 -> 1285 bytes
-rw-r--r--app/assets/images/emoji/handball.pngbin0 -> 1634 bytes
-rw-r--r--app/assets/images/emoji/handball_tone1.pngbin0 -> 1645 bytes
-rw-r--r--app/assets/images/emoji/handball_tone2.pngbin0 -> 1628 bytes
-rw-r--r--app/assets/images/emoji/handball_tone3.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/handball_tone4.pngbin0 -> 1634 bytes
-rw-r--r--app/assets/images/emoji/handball_tone5.pngbin0 -> 1606 bytes
-rw-r--r--app/assets/images/emoji/handshake.pngbin0 -> 1366 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone1.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone2.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone3.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone4.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone5.pngbin0 -> 1381 bytes
-rw-r--r--app/assets/images/emoji/hash.pngbin0 -> 604 bytes
-rw-r--r--app/assets/images/emoji/hatched_chick.pngbin0 -> 1174 bytes
-rw-r--r--app/assets/images/emoji/hatching_chick.pngbin0 -> 1598 bytes
-rw-r--r--app/assets/images/emoji/head_bandage.pngbin0 -> 1199 bytes
-rw-r--r--app/assets/images/emoji/headphones.pngbin0 -> 1202 bytes
-rw-r--r--app/assets/images/emoji/hear_no_evil.pngbin0 -> 1210 bytes
-rw-r--r--app/assets/images/emoji/heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/heart_decoration.pngbin0 -> 557 bytes
-rw-r--r--app/assets/images/emoji/heart_exclamation.pngbin0 -> 471 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes_cat.pngbin0 -> 1512 bytes
-rw-r--r--app/assets/images/emoji/heartbeat.pngbin0 -> 699 bytes
-rw-r--r--app/assets/images/emoji/heartpulse.pngbin0 -> 675 bytes
-rw-r--r--app/assets/images/emoji/hearts.pngbin0 -> 449 bytes
-rw-r--r--app/assets/images/emoji/heavy_check_mark.pngbin0 -> 438 bytes
-rw-r--r--app/assets/images/emoji/heavy_division_sign.pngbin0 -> 204 bytes
-rw-r--r--app/assets/images/emoji/heavy_dollar_sign.pngbin0 -> 429 bytes
-rw-r--r--app/assets/images/emoji/heavy_minus_sign.pngbin0 -> 108 bytes
-rw-r--r--app/assets/images/emoji/heavy_multiplication_x.pngbin0 -> 298 bytes
-rw-r--r--app/assets/images/emoji/heavy_plus_sign.pngbin0 -> 115 bytes
-rw-r--r--app/assets/images/emoji/helicopter.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/helmet_with_cross.pngbin0 -> 1014 bytes
-rw-r--r--app/assets/images/emoji/herb.pngbin0 -> 886 bytes
-rw-r--r--app/assets/images/emoji/hibiscus.pngbin0 -> 1815 bytes
-rw-r--r--app/assets/images/emoji/high_brightness.pngbin0 -> 474 bytes
-rw-r--r--app/assets/images/emoji/high_heel.pngbin0 -> 1008 bytes
-rw-r--r--app/assets/images/emoji/hockey.pngbin0 -> 1010 bytes
-rw-r--r--app/assets/images/emoji/hole.pngbin0 -> 1390 bytes
-rw-r--r--app/assets/images/emoji/homes.pngbin0 -> 981 bytes
-rw-r--r--app/assets/images/emoji/honey_pot.pngbin0 -> 1217 bytes
-rw-r--r--app/assets/images/emoji/horse.pngbin0 -> 1694 bytes
-rw-r--r--app/assets/images/emoji/horse_racing.pngbin0 -> 2096 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone1.pngbin0 -> 2099 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone2.pngbin0 -> 2103 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone3.pngbin0 -> 2090 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone4.pngbin0 -> 2090 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone5.pngbin0 -> 2085 bytes
-rw-r--r--app/assets/images/emoji/hospital.pngbin0 -> 530 bytes
-rw-r--r--app/assets/images/emoji/hot_pepper.pngbin0 -> 677 bytes
-rw-r--r--app/assets/images/emoji/hotdog.pngbin0 -> 1770 bytes
-rw-r--r--app/assets/images/emoji/hotel.pngbin0 -> 1322 bytes
-rw-r--r--app/assets/images/emoji/hotsprings.pngbin0 -> 733 bytes
-rw-r--r--app/assets/images/emoji/hourglass.pngbin0 -> 800 bytes
-rw-r--r--app/assets/images/emoji/hourglass_flowing_sand.pngbin0 -> 847 bytes
-rw-r--r--app/assets/images/emoji/house.pngbin0 -> 863 bytes
-rw-r--r--app/assets/images/emoji/house_abandoned.pngbin0 -> 1606 bytes
-rw-r--r--app/assets/images/emoji/house_with_garden.pngbin0 -> 1613 bytes
-rw-r--r--app/assets/images/emoji/hugging.pngbin0 -> 1425 bytes
-rw-r--r--app/assets/images/emoji/hushed.pngbin0 -> 634 bytes
-rw-r--r--app/assets/images/emoji/ice_cream.pngbin0 -> 1779 bytes
-rw-r--r--app/assets/images/emoji/ice_skate.pngbin0 -> 1574 bytes
-rw-r--r--app/assets/images/emoji/icecream.pngbin0 -> 1496 bytes
-rw-r--r--app/assets/images/emoji/id.pngbin0 -> 348 bytes
-rw-r--r--app/assets/images/emoji/ideograph_advantage.pngbin0 -> 716 bytes
-rw-r--r--app/assets/images/emoji/imp.pngbin0 -> 1988 bytes
-rw-r--r--app/assets/images/emoji/inbox_tray.pngbin0 -> 1029 bytes
-rw-r--r--app/assets/images/emoji/incoming_envelope.pngbin0 -> 1129 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person.pngbin0 -> 1580 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone1.pngbin0 -> 1597 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone2.pngbin0 -> 1590 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone3.pngbin0 -> 1580 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone4.pngbin0 -> 1572 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone5.pngbin0 -> 1588 bytes
-rw-r--r--app/assets/images/emoji/information_source.pngbin0 -> 506 bytes
-rw-r--r--app/assets/images/emoji/innocent.pngbin0 -> 935 bytes
-rw-r--r--app/assets/images/emoji/interrobang.pngbin0 -> 601 bytes
-rw-r--r--app/assets/images/emoji/iphone.pngbin0 -> 695 bytes
-rw-r--r--app/assets/images/emoji/island.pngbin0 -> 1273 bytes
-rw-r--r--app/assets/images/emoji/izakaya_lantern.pngbin0 -> 1227 bytes
-rw-r--r--app/assets/images/emoji/jack_o_lantern.pngbin0 -> 2289 bytes
-rw-r--r--app/assets/images/emoji/japan.pngbin0 -> 539 bytes
-rw-r--r--app/assets/images/emoji/japanese_castle.pngbin0 -> 1404 bytes
-rw-r--r--app/assets/images/emoji/japanese_goblin.pngbin0 -> 1561 bytes
-rw-r--r--app/assets/images/emoji/japanese_ogre.pngbin0 -> 1864 bytes
-rw-r--r--app/assets/images/emoji/jeans.pngbin0 -> 1158 bytes
-rw-r--r--app/assets/images/emoji/joy.pngbin0 -> 1136 bytes
-rw-r--r--app/assets/images/emoji/joy_cat.pngbin0 -> 1633 bytes
-rw-r--r--app/assets/images/emoji/joystick.pngbin0 -> 1039 bytes
-rw-r--r--app/assets/images/emoji/juggling.pngbin0 -> 1165 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone1.pngbin0 -> 1171 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone2.pngbin0 -> 1160 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone3.pngbin0 -> 1170 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone4.pngbin0 -> 1167 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone5.pngbin0 -> 1161 bytes
-rw-r--r--app/assets/images/emoji/kaaba.pngbin0 -> 1251 bytes
-rw-r--r--app/assets/images/emoji/key.pngbin0 -> 770 bytes
-rw-r--r--app/assets/images/emoji/key2.pngbin0 -> 593 bytes
-rw-r--r--app/assets/images/emoji/keyboard.pngbin0 -> 429 bytes
-rw-r--r--app/assets/images/emoji/kimono.pngbin0 -> 1527 bytes
-rw-r--r--app/assets/images/emoji/kiss.pngbin0 -> 842 bytes
-rw-r--r--app/assets/images/emoji/kiss_mm.pngbin0 -> 1269 bytes
-rw-r--r--app/assets/images/emoji/kiss_ww.pngbin0 -> 1149 bytes
-rw-r--r--app/assets/images/emoji/kissing.pngbin0 -> 738 bytes
-rw-r--r--app/assets/images/emoji/kissing_cat.pngbin0 -> 1468 bytes
-rw-r--r--app/assets/images/emoji/kissing_closed_eyes.pngbin0 -> 888 bytes
-rw-r--r--app/assets/images/emoji/kissing_heart.pngbin0 -> 843 bytes
-rw-r--r--app/assets/images/emoji/kissing_smiling_eyes.pngbin0 -> 648 bytes
-rw-r--r--app/assets/images/emoji/kiwi.pngbin0 -> 1892 bytes
-rw-r--r--app/assets/images/emoji/knife.pngbin0 -> 616 bytes
-rw-r--r--app/assets/images/emoji/koala.pngbin0 -> 1428 bytes
-rw-r--r--app/assets/images/emoji/koko.pngbin0 -> 266 bytes
-rw-r--r--app/assets/images/emoji/label.pngbin0 -> 669 bytes
-rw-r--r--app/assets/images/emoji/large_blue_circle.pngbin0 -> 371 bytes
-rw-r--r--app/assets/images/emoji/large_blue_diamond.pngbin0 -> 245 bytes
-rw-r--r--app/assets/images/emoji/large_orange_diamond.pngbin0 -> 248 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon.pngbin0 -> 1180 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon_with_face.pngbin0 -> 1030 bytes
-rw-r--r--app/assets/images/emoji/laughing.pngbin0 -> 901 bytes
-rw-r--r--app/assets/images/emoji/leaves.pngbin0 -> 993 bytes
-rw-r--r--app/assets/images/emoji/ledger.pngbin0 -> 1528 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist.pngbin0 -> 972 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone1.pngbin0 -> 960 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone2.pngbin0 -> 972 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone3.pngbin0 -> 960 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone4.pngbin0 -> 960 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone5.pngbin0 -> 976 bytes
-rw-r--r--app/assets/images/emoji/left_luggage.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/left_right_arrow.pngbin0 -> 495 bytes
-rw-r--r--app/assets/images/emoji/leftwards_arrow_with_hook.pngbin0 -> 643 bytes
-rw-r--r--app/assets/images/emoji/lemon.pngbin0 -> 1033 bytes
-rw-r--r--app/assets/images/emoji/leo.pngbin0 -> 745 bytes
-rw-r--r--app/assets/images/emoji/leopard.pngbin0 -> 2222 bytes
-rw-r--r--app/assets/images/emoji/level_slider.pngbin0 -> 454 bytes
-rw-r--r--app/assets/images/emoji/levitate.pngbin0 -> 914 bytes
-rw-r--r--app/assets/images/emoji/libra.pngbin0 -> 657 bytes
-rw-r--r--app/assets/images/emoji/lifter.pngbin0 -> 1356 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone1.pngbin0 -> 1346 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone2.pngbin0 -> 1347 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone3.pngbin0 -> 1339 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone4.pngbin0 -> 1343 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone5.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/emoji/light_rail.pngbin0 -> 902 bytes
-rw-r--r--app/assets/images/emoji/link.pngbin0 -> 477 bytes
-rw-r--r--app/assets/images/emoji/lion_face.pngbin0 -> 1728 bytes
-rw-r--r--app/assets/images/emoji/lips.pngbin0 -> 599 bytes
-rw-r--r--app/assets/images/emoji/lipstick.pngbin0 -> 549 bytes
-rw-r--r--app/assets/images/emoji/lizard.pngbin0 -> 1709 bytes
-rw-r--r--app/assets/images/emoji/lock.pngbin0 -> 986 bytes
-rw-r--r--app/assets/images/emoji/lock_with_ink_pen.pngbin0 -> 1123 bytes
-rw-r--r--app/assets/images/emoji/lollipop.pngbin0 -> 2164 bytes
-rw-r--r--app/assets/images/emoji/loop.pngbin0 -> 550 bytes
-rw-r--r--app/assets/images/emoji/loud_sound.pngbin0 -> 977 bytes
-rw-r--r--app/assets/images/emoji/loudspeaker.pngbin0 -> 1316 bytes
-rw-r--r--app/assets/images/emoji/love_hotel.pngbin0 -> 372 bytes
-rw-r--r--app/assets/images/emoji/love_letter.pngbin0 -> 923 bytes
-rw-r--r--app/assets/images/emoji/low_brightness.pngbin0 -> 431 bytes
-rw-r--r--app/assets/images/emoji/lying_face.pngbin0 -> 1103 bytes
-rw-r--r--app/assets/images/emoji/m.pngbin0 -> 500 bytes
-rw-r--r--app/assets/images/emoji/mag.pngbin0 -> 1240 bytes
-rw-r--r--app/assets/images/emoji/mag_right.pngbin0 -> 1251 bytes
-rw-r--r--app/assets/images/emoji/mahjong.pngbin0 -> 951 bytes
-rw-r--r--app/assets/images/emoji/mailbox.pngbin0 -> 1166 bytes
-rw-r--r--app/assets/images/emoji/mailbox_closed.pngbin0 -> 1192 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_mail.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_no_mail.pngbin0 -> 960 bytes
-rw-r--r--app/assets/images/emoji/man.pngbin0 -> 1092 bytes
-rw-r--r--app/assets/images/emoji/man_dancing.pngbin0 -> 1400 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone1.pngbin0 -> 1404 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone2.pngbin0 -> 1402 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone3.pngbin0 -> 1409 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone4.pngbin0 -> 1421 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone5.pngbin0 -> 1418 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone1.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone2.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone3.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone4.pngbin0 -> 1307 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone5.pngbin0 -> 1302 bytes
-rw-r--r--app/assets/images/emoji/man_tone1.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/man_tone2.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/man_tone3.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/man_tone4.pngbin0 -> 1069 bytes
-rw-r--r--app/assets/images/emoji/man_tone5.pngbin0 -> 1087 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao.pngbin0 -> 1339 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone1.pngbin0 -> 1328 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone2.pngbin0 -> 1332 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone3.pngbin0 -> 1329 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone4.pngbin0 -> 1325 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone5.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban.pngbin0 -> 1618 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone1.pngbin0 -> 1584 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone2.pngbin0 -> 1588 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone3.pngbin0 -> 1584 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone4.pngbin0 -> 1583 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone5.pngbin0 -> 1605 bytes
-rw-r--r--app/assets/images/emoji/mans_shoe.pngbin0 -> 1649 bytes
-rw-r--r--app/assets/images/emoji/map.pngbin0 -> 2352 bytes
-rw-r--r--app/assets/images/emoji/maple_leaf.pngbin0 -> 1117 bytes
-rw-r--r--app/assets/images/emoji/martial_arts_uniform.pngbin0 -> 1412 bytes
-rw-r--r--app/assets/images/emoji/mask.pngbin0 -> 1322 bytes
-rw-r--r--app/assets/images/emoji/massage.pngbin0 -> 1571 bytes
-rw-r--r--app/assets/images/emoji/massage_tone1.pngbin0 -> 1578 bytes
-rw-r--r--app/assets/images/emoji/massage_tone2.pngbin0 -> 1565 bytes
-rw-r--r--app/assets/images/emoji/massage_tone3.pngbin0 -> 1553 bytes
-rw-r--r--app/assets/images/emoji/massage_tone4.pngbin0 -> 1546 bytes
-rw-r--r--app/assets/images/emoji/massage_tone5.pngbin0 -> 1557 bytes
-rw-r--r--app/assets/images/emoji/meat_on_bone.pngbin0 -> 1465 bytes
-rw-r--r--app/assets/images/emoji/medal.pngbin0 -> 1700 bytes
-rw-r--r--app/assets/images/emoji/mega.pngbin0 -> 1751 bytes
-rw-r--r--app/assets/images/emoji/melon.pngbin0 -> 2005 bytes
-rw-r--r--app/assets/images/emoji/menorah.pngbin0 -> 1279 bytes
-rw-r--r--app/assets/images/emoji/mens.pngbin0 -> 561 bytes
-rw-r--r--app/assets/images/emoji/metal.pngbin0 -> 894 bytes
-rw-r--r--app/assets/images/emoji/metal_tone1.pngbin0 -> 894 bytes
-rw-r--r--app/assets/images/emoji/metal_tone2.pngbin0 -> 888 bytes
-rw-r--r--app/assets/images/emoji/metal_tone3.pngbin0 -> 894 bytes
-rw-r--r--app/assets/images/emoji/metal_tone4.pngbin0 -> 888 bytes
-rw-r--r--app/assets/images/emoji/metal_tone5.pngbin0 -> 894 bytes
-rw-r--r--app/assets/images/emoji/metro.pngbin0 -> 1020 bytes
-rw-r--r--app/assets/images/emoji/microphone.pngbin0 -> 1165 bytes
-rw-r--r--app/assets/images/emoji/microphone2.pngbin0 -> 839 bytes
-rw-r--r--app/assets/images/emoji/microscope.pngbin0 -> 1113 bytes
-rw-r--r--app/assets/images/emoji/middle_finger.pngbin0 -> 893 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone1.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone2.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone3.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone4.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone5.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/military_medal.pngbin0 -> 949 bytes
-rw-r--r--app/assets/images/emoji/milk.pngbin0 -> 1224 bytes
-rw-r--r--app/assets/images/emoji/milky_way.pngbin0 -> 622 bytes
-rw-r--r--app/assets/images/emoji/minibus.pngbin0 -> 1256 bytes
-rw-r--r--app/assets/images/emoji/minidisc.pngbin0 -> 522 bytes
-rw-r--r--app/assets/images/emoji/mobile_phone_off.pngbin0 -> 621 bytes
-rw-r--r--app/assets/images/emoji/money_mouth.pngbin0 -> 967 bytes
-rw-r--r--app/assets/images/emoji/money_with_wings.pngbin0 -> 2327 bytes
-rw-r--r--app/assets/images/emoji/moneybag.pngbin0 -> 2310 bytes
-rw-r--r--app/assets/images/emoji/monkey.pngbin0 -> 1348 bytes
-rw-r--r--app/assets/images/emoji/monkey_face.pngbin0 -> 1022 bytes
-rw-r--r--app/assets/images/emoji/monorail.pngbin0 -> 1068 bytes
-rw-r--r--app/assets/images/emoji/mortar_board.pngbin0 -> 710 bytes
-rw-r--r--app/assets/images/emoji/mosque.pngbin0 -> 984 bytes
-rw-r--r--app/assets/images/emoji/motor_scooter.pngbin0 -> 1207 bytes
-rw-r--r--app/assets/images/emoji/motorboat.pngbin0 -> 990 bytes
-rw-r--r--app/assets/images/emoji/motorcycle.pngbin0 -> 2081 bytes
-rw-r--r--app/assets/images/emoji/motorway.pngbin0 -> 1102 bytes
-rw-r--r--app/assets/images/emoji/mount_fuji.pngbin0 -> 881 bytes
-rw-r--r--app/assets/images/emoji/mountain.pngbin0 -> 1409 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist.pngbin0 -> 2288 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone1.pngbin0 -> 2294 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone2.pngbin0 -> 2298 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone3.pngbin0 -> 2284 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone4.pngbin0 -> 2288 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone5.pngbin0 -> 2281 bytes
-rw-r--r--app/assets/images/emoji/mountain_cableway.pngbin0 -> 811 bytes
-rw-r--r--app/assets/images/emoji/mountain_railway.pngbin0 -> 1317 bytes
-rw-r--r--app/assets/images/emoji/mountain_snow.pngbin0 -> 1193 bytes
-rw-r--r--app/assets/images/emoji/mouse.pngbin0 -> 1245 bytes
-rw-r--r--app/assets/images/emoji/mouse2.pngbin0 -> 1324 bytes
-rw-r--r--app/assets/images/emoji/mouse_three_button.pngbin0 -> 934 bytes
-rw-r--r--app/assets/images/emoji/movie_camera.pngbin0 -> 576 bytes
-rw-r--r--app/assets/images/emoji/moyai.pngbin0 -> 1593 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus.pngbin0 -> 2206 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone1.pngbin0 -> 1999 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone2.pngbin0 -> 2006 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone3.pngbin0 -> 2017 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone4.pngbin0 -> 2016 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone5.pngbin0 -> 2016 bytes
-rw-r--r--app/assets/images/emoji/muscle.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone1.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone2.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone3.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone4.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone5.pngbin0 -> 1012 bytes
-rw-r--r--app/assets/images/emoji/mushroom.pngbin0 -> 1024 bytes
-rw-r--r--app/assets/images/emoji/musical_keyboard.pngbin0 -> 1695 bytes
-rw-r--r--app/assets/images/emoji/musical_note.pngbin0 -> 419 bytes
-rw-r--r--app/assets/images/emoji/musical_score.pngbin0 -> 1289 bytes
-rw-r--r--app/assets/images/emoji/mute.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/nail_care.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone1.pngbin0 -> 1712 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone2.pngbin0 -> 1711 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone3.pngbin0 -> 1727 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone4.pngbin0 -> 1728 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone5.pngbin0 -> 1716 bytes
-rw-r--r--app/assets/images/emoji/name_badge.pngbin0 -> 632 bytes
-rw-r--r--app/assets/images/emoji/nauseated_face.pngbin0 -> 965 bytes
-rw-r--r--app/assets/images/emoji/necktie.pngbin0 -> 995 bytes
-rw-r--r--app/assets/images/emoji/negative_squared_cross_mark.pngbin0 -> 370 bytes
-rw-r--r--app/assets/images/emoji/nerd.pngbin0 -> 975 bytes
-rw-r--r--app/assets/images/emoji/neutral_face.pngbin0 -> 517 bytes
-rw-r--r--app/assets/images/emoji/new.pngbin0 -> 486 bytes
-rw-r--r--app/assets/images/emoji/new_moon.pngbin0 -> 829 bytes
-rw-r--r--app/assets/images/emoji/new_moon_with_face.pngbin0 -> 975 bytes
-rw-r--r--app/assets/images/emoji/newspaper.pngbin0 -> 1178 bytes
-rw-r--r--app/assets/images/emoji/newspaper2.pngbin0 -> 1046 bytes
-rw-r--r--app/assets/images/emoji/ng.pngbin0 -> 445 bytes
-rw-r--r--app/assets/images/emoji/night_with_stars.pngbin0 -> 835 bytes
-rw-r--r--app/assets/images/emoji/nine.pngbin0 -> 607 bytes
-rw-r--r--app/assets/images/emoji/no_bell.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/no_bicycles.pngbin0 -> 998 bytes
-rw-r--r--app/assets/images/emoji/no_entry.pngbin0 -> 377 bytes
-rw-r--r--app/assets/images/emoji/no_entry_sign.pngbin0 -> 555 bytes
-rw-r--r--app/assets/images/emoji/no_good.pngbin0 -> 1750 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone1.pngbin0 -> 1767 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone2.pngbin0 -> 1756 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone3.pngbin0 -> 1766 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone4.pngbin0 -> 1782 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone5.pngbin0 -> 1784 bytes
-rw-r--r--app/assets/images/emoji/no_mobile_phones.pngbin0 -> 790 bytes
-rw-r--r--app/assets/images/emoji/no_mouth.pngbin0 -> 465 bytes
-rw-r--r--app/assets/images/emoji/no_pedestrians.pngbin0 -> 875 bytes
-rw-r--r--app/assets/images/emoji/no_smoking.pngbin0 -> 1136 bytes
-rw-r--r--app/assets/images/emoji/non-potable_water.pngbin0 -> 827 bytes
-rw-r--r--app/assets/images/emoji/nose.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/nose_tone1.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/nose_tone2.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/nose_tone3.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/nose_tone4.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/nose_tone5.pngbin0 -> 703 bytes
-rw-r--r--app/assets/images/emoji/notebook.pngbin0 -> 1215 bytes
-rw-r--r--app/assets/images/emoji/notebook_with_decorative_cover.pngbin0 -> 1782 bytes
-rw-r--r--app/assets/images/emoji/notepad_spiral.pngbin0 -> 1377 bytes
-rw-r--r--app/assets/images/emoji/notes.pngbin0 -> 501 bytes
-rw-r--r--app/assets/images/emoji/nut_and_bolt.pngbin0 -> 899 bytes
-rw-r--r--app/assets/images/emoji/o.pngbin0 -> 475 bytes
-rw-r--r--app/assets/images/emoji/o2.pngbin0 -> 425 bytes
-rw-r--r--app/assets/images/emoji/ocean.pngbin0 -> 1018 bytes
-rw-r--r--app/assets/images/emoji/octagonal_sign.pngbin0 -> 260 bytes
-rw-r--r--app/assets/images/emoji/octopus.pngbin0 -> 1188 bytes
-rw-r--r--app/assets/images/emoji/oden.pngbin0 -> 794 bytes
-rw-r--r--app/assets/images/emoji/office.pngbin0 -> 524 bytes
-rw-r--r--app/assets/images/emoji/oil.pngbin0 -> 674 bytes
-rw-r--r--app/assets/images/emoji/ok.pngbin0 -> 511 bytes
-rw-r--r--app/assets/images/emoji/ok_hand.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone1.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone2.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone3.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone4.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone5.pngbin0 -> 979 bytes
-rw-r--r--app/assets/images/emoji/ok_woman.pngbin0 -> 1696 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone1.pngbin0 -> 1696 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone2.pngbin0 -> 1694 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone3.pngbin0 -> 1675 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone4.pngbin0 -> 1684 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone5.pngbin0 -> 1696 bytes
-rw-r--r--app/assets/images/emoji/older_man.pngbin0 -> 1253 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone1.pngbin0 -> 1253 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone2.pngbin0 -> 1253 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone3.pngbin0 -> 1253 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone4.pngbin0 -> 1254 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone5.pngbin0 -> 1254 bytes
-rw-r--r--app/assets/images/emoji/older_woman.pngbin0 -> 1472 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone1.pngbin0 -> 1562 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone2.pngbin0 -> 1564 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone3.pngbin0 -> 1555 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone4.pngbin0 -> 1562 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone5.pngbin0 -> 1544 bytes
-rw-r--r--app/assets/images/emoji/om_symbol.pngbin0 -> 773 bytes
-rw-r--r--app/assets/images/emoji/on.pngbin0 -> 459 bytes
-rw-r--r--app/assets/images/emoji/oncoming_automobile.pngbin0 -> 1238 bytes
-rw-r--r--app/assets/images/emoji/oncoming_bus.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/oncoming_police_car.pngbin0 -> 1547 bytes
-rw-r--r--app/assets/images/emoji/oncoming_taxi.pngbin0 -> 1405 bytes
-rw-r--r--app/assets/images/emoji/one.pngbin0 -> 442 bytes
-rw-r--r--app/assets/images/emoji/open_file_folder.pngbin0 -> 755 bytes
-rw-r--r--app/assets/images/emoji/open_hands.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone1.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone2.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone3.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone4.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone5.pngbin0 -> 1053 bytes
-rw-r--r--app/assets/images/emoji/open_mouth.pngbin0 -> 575 bytes
-rw-r--r--app/assets/images/emoji/ophiuchus.pngbin0 -> 723 bytes
-rw-r--r--app/assets/images/emoji/orange_book.pngbin0 -> 1329 bytes
-rw-r--r--app/assets/images/emoji/orthodox_cross.pngbin0 -> 239 bytes
-rw-r--r--app/assets/images/emoji/outbox_tray.pngbin0 -> 1002 bytes
-rw-r--r--app/assets/images/emoji/owl.pngbin0 -> 2045 bytes
-rw-r--r--app/assets/images/emoji/ox.pngbin0 -> 1436 bytes
-rw-r--r--app/assets/images/emoji/package.pngbin0 -> 950 bytes
-rw-r--r--app/assets/images/emoji/page_facing_up.pngbin0 -> 1110 bytes
-rw-r--r--app/assets/images/emoji/page_with_curl.pngbin0 -> 1157 bytes
-rw-r--r--app/assets/images/emoji/pager.pngbin0 -> 553 bytes
-rw-r--r--app/assets/images/emoji/paintbrush.pngbin0 -> 950 bytes
-rw-r--r--app/assets/images/emoji/palm_tree.pngbin0 -> 1450 bytes
-rw-r--r--app/assets/images/emoji/pancakes.pngbin0 -> 3661 bytes
-rw-r--r--app/assets/images/emoji/panda_face.pngbin0 -> 1478 bytes
-rw-r--r--app/assets/images/emoji/paperclip.pngbin0 -> 439 bytes
-rw-r--r--app/assets/images/emoji/paperclips.pngbin0 -> 642 bytes
-rw-r--r--app/assets/images/emoji/park.pngbin0 -> 929 bytes
-rw-r--r--app/assets/images/emoji/parking.pngbin0 -> 385 bytes
-rw-r--r--app/assets/images/emoji/part_alternation_mark.pngbin0 -> 521 bytes
-rw-r--r--app/assets/images/emoji/partly_sunny.pngbin0 -> 977 bytes
-rw-r--r--app/assets/images/emoji/passport_control.pngbin0 -> 683 bytes
-rw-r--r--app/assets/images/emoji/pause_button.pngbin0 -> 395 bytes
-rw-r--r--app/assets/images/emoji/peace.pngbin0 -> 933 bytes
-rw-r--r--app/assets/images/emoji/peach.pngbin0 -> 1189 bytes
-rw-r--r--app/assets/images/emoji/peanuts.pngbin0 -> 3266 bytes
-rw-r--r--app/assets/images/emoji/pear.pngbin0 -> 747 bytes
-rw-r--r--app/assets/images/emoji/pen_ballpoint.pngbin0 -> 696 bytes
-rw-r--r--app/assets/images/emoji/pen_fountain.pngbin0 -> 623 bytes
-rw-r--r--app/assets/images/emoji/pencil.pngbin0 -> 1624 bytes
-rw-r--r--app/assets/images/emoji/pencil2.pngbin0 -> 654 bytes
-rw-r--r--app/assets/images/emoji/penguin.pngbin0 -> 1034 bytes
-rw-r--r--app/assets/images/emoji/pensive.pngbin0 -> 718 bytes
-rw-r--r--app/assets/images/emoji/performing_arts.pngbin0 -> 1971 bytes
-rw-r--r--app/assets/images/emoji/persevere.pngbin0 -> 891 bytes
-rw-r--r--app/assets/images/emoji/person_frowning.pngbin0 -> 1148 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone1.pngbin0 -> 1141 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone2.pngbin0 -> 1141 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone3.pngbin0 -> 1141 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone4.pngbin0 -> 1109 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone5.pngbin0 -> 1114 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair.pngbin0 -> 1205 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone1.pngbin0 -> 1181 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone2.pngbin0 -> 1181 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone3.pngbin0 -> 1181 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone4.pngbin0 -> 1189 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone5.pngbin0 -> 1214 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face.pngbin0 -> 1297 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone1.pngbin0 -> 1309 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone2.pngbin0 -> 1292 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone3.pngbin0 -> 1305 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone4.pngbin0 -> 1296 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone5.pngbin0 -> 1303 bytes
-rw-r--r--app/assets/images/emoji/pick.pngbin0 -> 1023 bytes
-rw-r--r--app/assets/images/emoji/pig.pngbin0 -> 1138 bytes
-rw-r--r--app/assets/images/emoji/pig2.pngbin0 -> 1548 bytes
-rw-r--r--app/assets/images/emoji/pig_nose.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/pill.pngbin0 -> 442 bytes
-rw-r--r--app/assets/images/emoji/pineapple.pngbin0 -> 1642 bytes
-rw-r--r--app/assets/images/emoji/ping_pong.pngbin0 -> 823 bytes
-rw-r--r--app/assets/images/emoji/pisces.pngbin0 -> 678 bytes
-rw-r--r--app/assets/images/emoji/pizza.pngbin0 -> 2008 bytes
-rw-r--r--app/assets/images/emoji/place_of_worship.pngbin0 -> 487 bytes
-rw-r--r--app/assets/images/emoji/play_pause.pngbin0 -> 509 bytes
-rw-r--r--app/assets/images/emoji/point_down.pngbin0 -> 853 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone1.pngbin0 -> 856 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone2.pngbin0 -> 856 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone3.pngbin0 -> 858 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone4.pngbin0 -> 856 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone5.pngbin0 -> 856 bytes
-rw-r--r--app/assets/images/emoji/point_left.pngbin0 -> 825 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone1.pngbin0 -> 832 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone2.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone3.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone4.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone5.pngbin0 -> 832 bytes
-rw-r--r--app/assets/images/emoji/point_right.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone1.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone2.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone3.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone4.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone5.pngbin0 -> 805 bytes
-rw-r--r--app/assets/images/emoji/point_up.pngbin0 -> 819 bytes
-rw-r--r--app/assets/images/emoji/point_up_2.pngbin0 -> 822 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone1.pngbin0 -> 822 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone2.pngbin0 -> 822 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone3.pngbin0 -> 871 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone4.pngbin0 -> 822 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone5.pngbin0 -> 822 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone1.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone2.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone3.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone4.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone5.pngbin0 -> 820 bytes
-rw-r--r--app/assets/images/emoji/police_car.pngbin0 -> 1431 bytes
-rw-r--r--app/assets/images/emoji/poodle.pngbin0 -> 1531 bytes
-rw-r--r--app/assets/images/emoji/poop.pngbin0 -> 1273 bytes
-rw-r--r--app/assets/images/emoji/popcorn.pngbin0 -> 1843 bytes
-rw-r--r--app/assets/images/emoji/post_office.pngbin0 -> 676 bytes
-rw-r--r--app/assets/images/emoji/postal_horn.pngbin0 -> 809 bytes
-rw-r--r--app/assets/images/emoji/postbox.pngbin0 -> 1077 bytes
-rw-r--r--app/assets/images/emoji/potable_water.pngbin0 -> 633 bytes
-rw-r--r--app/assets/images/emoji/potato.pngbin0 -> 1246 bytes
-rw-r--r--app/assets/images/emoji/pouch.pngbin0 -> 1259 bytes
-rw-r--r--app/assets/images/emoji/poultry_leg.pngbin0 -> 925 bytes
-rw-r--r--app/assets/images/emoji/pound.pngbin0 -> 452 bytes
-rw-r--r--app/assets/images/emoji/pouting_cat.pngbin0 -> 1675 bytes
-rw-r--r--app/assets/images/emoji/pray.pngbin0 -> 1122 bytes
-rw-r--r--app/assets/images/emoji/pray_tone1.pngbin0 -> 1131 bytes
-rw-r--r--app/assets/images/emoji/pray_tone2.pngbin0 -> 1134 bytes
-rw-r--r--app/assets/images/emoji/pray_tone3.pngbin0 -> 1137 bytes
-rw-r--r--app/assets/images/emoji/pray_tone4.pngbin0 -> 1126 bytes
-rw-r--r--app/assets/images/emoji/pray_tone5.pngbin0 -> 1117 bytes
-rw-r--r--app/assets/images/emoji/prayer_beads.pngbin0 -> 1059 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman.pngbin0 -> 1252 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone1.pngbin0 -> 1255 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone2.pngbin0 -> 1246 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone3.pngbin0 -> 1237 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone4.pngbin0 -> 1246 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone5.pngbin0 -> 1235 bytes
-rw-r--r--app/assets/images/emoji/prince.pngbin0 -> 1616 bytes
-rw-r--r--app/assets/images/emoji/prince_tone1.pngbin0 -> 1618 bytes
-rw-r--r--app/assets/images/emoji/prince_tone2.pngbin0 -> 1621 bytes
-rw-r--r--app/assets/images/emoji/prince_tone3.pngbin0 -> 1619 bytes
-rw-r--r--app/assets/images/emoji/prince_tone4.pngbin0 -> 1619 bytes
-rw-r--r--app/assets/images/emoji/prince_tone5.pngbin0 -> 1616 bytes
-rw-r--r--app/assets/images/emoji/princess.pngbin0 -> 1812 bytes
-rw-r--r--app/assets/images/emoji/princess_tone1.pngbin0 -> 1812 bytes
-rw-r--r--app/assets/images/emoji/princess_tone2.pngbin0 -> 1805 bytes
-rw-r--r--app/assets/images/emoji/princess_tone3.pngbin0 -> 1805 bytes
-rw-r--r--app/assets/images/emoji/princess_tone4.pngbin0 -> 1813 bytes
-rw-r--r--app/assets/images/emoji/princess_tone5.pngbin0 -> 1812 bytes
-rw-r--r--app/assets/images/emoji/printer.pngbin0 -> 926 bytes
-rw-r--r--app/assets/images/emoji/projector.pngbin0 -> 943 bytes
-rw-r--r--app/assets/images/emoji/punch.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/punch_tone1.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/punch_tone2.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/punch_tone3.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/punch_tone4.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/punch_tone5.pngbin0 -> 838 bytes
-rw-r--r--app/assets/images/emoji/purple_heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/purse.pngbin0 -> 1558 bytes
-rw-r--r--app/assets/images/emoji/pushpin.pngbin0 -> 640 bytes
-rw-r--r--app/assets/images/emoji/put_litter_in_its_place.pngbin0 -> 650 bytes
-rw-r--r--app/assets/images/emoji/question.pngbin0 -> 449 bytes
-rw-r--r--app/assets/images/emoji/rabbit.pngbin0 -> 1660 bytes
-rw-r--r--app/assets/images/emoji/rabbit2.pngbin0 -> 1805 bytes
-rw-r--r--app/assets/images/emoji/race_car.pngbin0 -> 2140 bytes
-rw-r--r--app/assets/images/emoji/racehorse.pngbin0 -> 1401 bytes
-rw-r--r--app/assets/images/emoji/radio.pngbin0 -> 851 bytes
-rw-r--r--app/assets/images/emoji/radio_button.pngbin0 -> 674 bytes
-rw-r--r--app/assets/images/emoji/radioactive.pngbin0 -> 858 bytes
-rw-r--r--app/assets/images/emoji/rage.pngbin0 -> 845 bytes
-rw-r--r--app/assets/images/emoji/railway_car.pngbin0 -> 847 bytes
-rw-r--r--app/assets/images/emoji/railway_track.pngbin0 -> 1550 bytes
-rw-r--r--app/assets/images/emoji/rainbow.pngbin0 -> 1299 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone1.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone2.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone3.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone4.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone5.pngbin0 -> 848 bytes
-rw-r--r--app/assets/images/emoji/raised_hand.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone1.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone2.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone3.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone4.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone5.pngbin0 -> 791 bytes
-rw-r--r--app/assets/images/emoji/raised_hands.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone1.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone2.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone3.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone4.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone5.pngbin0 -> 1098 bytes
-rw-r--r--app/assets/images/emoji/raising_hand.pngbin0 -> 1664 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone1.pngbin0 -> 1678 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone2.pngbin0 -> 1665 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone3.pngbin0 -> 1657 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone4.pngbin0 -> 1657 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone5.pngbin0 -> 1661 bytes
-rw-r--r--app/assets/images/emoji/ram.pngbin0 -> 1951 bytes
-rw-r--r--app/assets/images/emoji/ramen.pngbin0 -> 1992 bytes
-rw-r--r--app/assets/images/emoji/rat.pngbin0 -> 1193 bytes
-rw-r--r--app/assets/images/emoji/record_button.pngbin0 -> 475 bytes
-rw-r--r--app/assets/images/emoji/recycle.pngbin0 -> 914 bytes
-rw-r--r--app/assets/images/emoji/red_car.pngbin0 -> 1065 bytes
-rw-r--r--app/assets/images/emoji/red_circle.pngbin0 -> 374 bytes
-rw-r--r--app/assets/images/emoji/registered.pngbin0 -> 547 bytes
-rw-r--r--app/assets/images/emoji/relaxed.pngbin0 -> 636 bytes
-rw-r--r--app/assets/images/emoji/relieved.pngbin0 -> 785 bytes
-rw-r--r--app/assets/images/emoji/reminder_ribbon.pngbin0 -> 921 bytes
-rw-r--r--app/assets/images/emoji/repeat.pngbin0 -> 644 bytes
-rw-r--r--app/assets/images/emoji/repeat_one.pngbin0 -> 688 bytes
-rw-r--r--app/assets/images/emoji/restroom.pngbin0 -> 676 bytes
-rw-r--r--app/assets/images/emoji/revolving_hearts.pngbin0 -> 920 bytes
-rw-r--r--app/assets/images/emoji/rewind.pngbin0 -> 523 bytes
-rw-r--r--app/assets/images/emoji/rhino.pngbin0 -> 1558 bytes
-rw-r--r--app/assets/images/emoji/ribbon.pngbin0 -> 968 bytes
-rw-r--r--app/assets/images/emoji/rice.pngbin0 -> 1195 bytes
-rw-r--r--app/assets/images/emoji/rice_ball.pngbin0 -> 1091 bytes
-rw-r--r--app/assets/images/emoji/rice_cracker.pngbin0 -> 1443 bytes
-rw-r--r--app/assets/images/emoji/rice_scene.pngbin0 -> 1349 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist.pngbin0 -> 975 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone1.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone2.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone3.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone4.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone5.pngbin0 -> 964 bytes
-rw-r--r--app/assets/images/emoji/ring.pngbin0 -> 1113 bytes
-rw-r--r--app/assets/images/emoji/robot.pngbin0 -> 1228 bytes
-rw-r--r--app/assets/images/emoji/rocket.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/rofl.pngbin0 -> 1760 bytes
-rw-r--r--app/assets/images/emoji/roller_coaster.pngbin0 -> 1723 bytes
-rw-r--r--app/assets/images/emoji/rolling_eyes.pngbin0 -> 743 bytes
-rw-r--r--app/assets/images/emoji/rooster.pngbin0 -> 1333 bytes
-rw-r--r--app/assets/images/emoji/rose.pngbin0 -> 1182 bytes
-rw-r--r--app/assets/images/emoji/rosette.pngbin0 -> 1023 bytes
-rw-r--r--app/assets/images/emoji/rotating_light.pngbin0 -> 1969 bytes
-rw-r--r--app/assets/images/emoji/round_pushpin.pngbin0 -> 455 bytes
-rw-r--r--app/assets/images/emoji/rowboat.pngbin0 -> 1963 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone1.pngbin0 -> 1971 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone2.pngbin0 -> 1972 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone3.pngbin0 -> 1967 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone4.pngbin0 -> 1974 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone5.pngbin0 -> 1971 bytes
-rw-r--r--app/assets/images/emoji/rugby_football.pngbin0 -> 1618 bytes
-rw-r--r--app/assets/images/emoji/runner.pngbin0 -> 1161 bytes
-rw-r--r--app/assets/images/emoji/runner_tone1.pngbin0 -> 1163 bytes
-rw-r--r--app/assets/images/emoji/runner_tone2.pngbin0 -> 1162 bytes
-rw-r--r--app/assets/images/emoji/runner_tone3.pngbin0 -> 1151 bytes
-rw-r--r--app/assets/images/emoji/runner_tone4.pngbin0 -> 1156 bytes
-rw-r--r--app/assets/images/emoji/runner_tone5.pngbin0 -> 1145 bytes
-rw-r--r--app/assets/images/emoji/running_shirt_with_sash.pngbin0 -> 784 bytes
-rw-r--r--app/assets/images/emoji/sa.pngbin0 -> 420 bytes
-rw-r--r--app/assets/images/emoji/sagittarius.pngbin0 -> 602 bytes
-rw-r--r--app/assets/images/emoji/sailboat.pngbin0 -> 1274 bytes
-rw-r--r--app/assets/images/emoji/sake.pngbin0 -> 826 bytes
-rw-r--r--app/assets/images/emoji/salad.pngbin0 -> 2398 bytes
-rw-r--r--app/assets/images/emoji/sandal.pngbin0 -> 1180 bytes
-rw-r--r--app/assets/images/emoji/santa.pngbin0 -> 1585 bytes
-rw-r--r--app/assets/images/emoji/santa_tone1.pngbin0 -> 1585 bytes
-rw-r--r--app/assets/images/emoji/santa_tone2.pngbin0 -> 1578 bytes
-rw-r--r--app/assets/images/emoji/santa_tone3.pngbin0 -> 1578 bytes
-rw-r--r--app/assets/images/emoji/santa_tone4.pngbin0 -> 1578 bytes
-rw-r--r--app/assets/images/emoji/santa_tone5.pngbin0 -> 1578 bytes
-rw-r--r--app/assets/images/emoji/satellite.pngbin0 -> 1173 bytes
-rw-r--r--app/assets/images/emoji/satellite_orbital.pngbin0 -> 762 bytes
-rw-r--r--app/assets/images/emoji/saxophone.pngbin0 -> 1442 bytes
-rw-r--r--app/assets/images/emoji/scales.pngbin0 -> 1181 bytes
-rw-r--r--app/assets/images/emoji/school.pngbin0 -> 1234 bytes
-rw-r--r--app/assets/images/emoji/school_satchel.pngbin0 -> 1490 bytes
-rw-r--r--app/assets/images/emoji/scissors.pngbin0 -> 937 bytes
-rw-r--r--app/assets/images/emoji/scooter.pngbin0 -> 1228 bytes
-rw-r--r--app/assets/images/emoji/scorpion.pngbin0 -> 1503 bytes
-rw-r--r--app/assets/images/emoji/scorpius.pngbin0 -> 612 bytes
-rw-r--r--app/assets/images/emoji/scream.pngbin0 -> 1583 bytes
-rw-r--r--app/assets/images/emoji/scream_cat.pngbin0 -> 2120 bytes
-rw-r--r--app/assets/images/emoji/scroll.pngbin0 -> 989 bytes
-rw-r--r--app/assets/images/emoji/seat.pngbin0 -> 884 bytes
-rw-r--r--app/assets/images/emoji/second_place.pngbin0 -> 1511 bytes
-rw-r--r--app/assets/images/emoji/secret.pngbin0 -> 857 bytes
-rw-r--r--app/assets/images/emoji/see_no_evil.pngbin0 -> 1227 bytes
-rw-r--r--app/assets/images/emoji/seedling.pngbin0 -> 749 bytes
-rw-r--r--app/assets/images/emoji/selfie.pngbin0 -> 1160 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone1.pngbin0 -> 1166 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone2.pngbin0 -> 1167 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone3.pngbin0 -> 1154 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone4.pngbin0 -> 1153 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone5.pngbin0 -> 1148 bytes
-rw-r--r--app/assets/images/emoji/seven.pngbin0 -> 522 bytes
-rw-r--r--app/assets/images/emoji/shallow_pan_of_food.pngbin0 -> 1738 bytes
-rw-r--r--app/assets/images/emoji/shamrock.pngbin0 -> 1023 bytes
-rw-r--r--app/assets/images/emoji/shark.pngbin0 -> 1811 bytes
-rw-r--r--app/assets/images/emoji/shaved_ice.pngbin0 -> 997 bytes
-rw-r--r--app/assets/images/emoji/sheep.pngbin0 -> 1372 bytes
-rw-r--r--app/assets/images/emoji/shell.pngbin0 -> 1497 bytes
-rw-r--r--app/assets/images/emoji/shield.pngbin0 -> 1602 bytes
-rw-r--r--app/assets/images/emoji/shinto_shrine.pngbin0 -> 579 bytes
-rw-r--r--app/assets/images/emoji/ship.pngbin0 -> 1405 bytes
-rw-r--r--app/assets/images/emoji/shirt.pngbin0 -> 670 bytes
-rw-r--r--app/assets/images/emoji/shopping_bags.pngbin0 -> 1234 bytes
-rw-r--r--app/assets/images/emoji/shopping_cart.pngbin0 -> 1072 bytes
-rw-r--r--app/assets/images/emoji/shower.pngbin0 -> 2537 bytes
-rw-r--r--app/assets/images/emoji/shrimp.pngbin0 -> 1376 bytes
-rw-r--r--app/assets/images/emoji/shrug.pngbin0 -> 1671 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone1.pngbin0 -> 1676 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone2.pngbin0 -> 1671 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone3.pngbin0 -> 1675 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone4.pngbin0 -> 1641 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone5.pngbin0 -> 1634 bytes
-rw-r--r--app/assets/images/emoji/signal_strength.pngbin0 -> 445 bytes
-rw-r--r--app/assets/images/emoji/six.pngbin0 -> 612 bytes
-rw-r--r--app/assets/images/emoji/six_pointed_star.pngbin0 -> 540 bytes
-rw-r--r--app/assets/images/emoji/ski.pngbin0 -> 1762 bytes
-rw-r--r--app/assets/images/emoji/skier.pngbin0 -> 1539 bytes
-rw-r--r--app/assets/images/emoji/skull.pngbin0 -> 628 bytes
-rw-r--r--app/assets/images/emoji/skull_crossbones.pngbin0 -> 726 bytes
-rw-r--r--app/assets/images/emoji/sleeping.pngbin0 -> 1075 bytes
-rw-r--r--app/assets/images/emoji/sleeping_accommodation.pngbin0 -> 926 bytes
-rw-r--r--app/assets/images/emoji/sleepy.pngbin0 -> 1185 bytes
-rw-r--r--app/assets/images/emoji/slight_frown.pngbin0 -> 580 bytes
-rw-r--r--app/assets/images/emoji/slight_smile.pngbin0 -> 600 bytes
-rw-r--r--app/assets/images/emoji/slot_machine.pngbin0 -> 1648 bytes
-rw-r--r--app/assets/images/emoji/small_blue_diamond.pngbin0 -> 191 bytes
-rw-r--r--app/assets/images/emoji/small_orange_diamond.pngbin0 -> 194 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle.pngbin0 -> 273 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle_down.pngbin0 -> 291 bytes
-rw-r--r--app/assets/images/emoji/smile.pngbin0 -> 737 bytes
-rw-r--r--app/assets/images/emoji/smile_cat.pngbin0 -> 1405 bytes
-rw-r--r--app/assets/images/emoji/smiley.pngbin0 -> 686 bytes
-rw-r--r--app/assets/images/emoji/smiley_cat.pngbin0 -> 1669 bytes
-rw-r--r--app/assets/images/emoji/smiling_imp.pngbin0 -> 1078 bytes
-rw-r--r--app/assets/images/emoji/smirk.pngbin0 -> 775 bytes
-rw-r--r--app/assets/images/emoji/smirk_cat.pngbin0 -> 1663 bytes
-rw-r--r--app/assets/images/emoji/smoking.pngbin0 -> 417 bytes
-rw-r--r--app/assets/images/emoji/snail.pngbin0 -> 1731 bytes
-rw-r--r--app/assets/images/emoji/snake.pngbin0 -> 1575 bytes
-rw-r--r--app/assets/images/emoji/sneezing_face.pngbin0 -> 1289 bytes
-rw-r--r--app/assets/images/emoji/snowboarder.pngbin0 -> 2020 bytes
-rw-r--r--app/assets/images/emoji/snowflake.pngbin0 -> 691 bytes
-rw-r--r--app/assets/images/emoji/snowman.pngbin0 -> 1481 bytes
-rw-r--r--app/assets/images/emoji/snowman2.pngbin0 -> 2176 bytes
-rw-r--r--app/assets/images/emoji/sob.pngbin0 -> 1236 bytes
-rw-r--r--app/assets/images/emoji/soccer.pngbin0 -> 1034 bytes
-rw-r--r--app/assets/images/emoji/soon.pngbin0 -> 483 bytes
-rw-r--r--app/assets/images/emoji/sos.pngbin0 -> 604 bytes
-rw-r--r--app/assets/images/emoji/sound.pngbin0 -> 690 bytes
-rw-r--r--app/assets/images/emoji/space_invader.pngbin0 -> 1325 bytes
-rw-r--r--app/assets/images/emoji/spades.pngbin0 -> 454 bytes
-rw-r--r--app/assets/images/emoji/spaghetti.pngbin0 -> 1796 bytes
-rw-r--r--app/assets/images/emoji/sparkle.pngbin0 -> 663 bytes
-rw-r--r--app/assets/images/emoji/sparkler.pngbin0 -> 910 bytes
-rw-r--r--app/assets/images/emoji/sparkles.pngbin0 -> 651 bytes
-rw-r--r--app/assets/images/emoji/sparkling_heart.pngbin0 -> 821 bytes
-rw-r--r--app/assets/images/emoji/speak_no_evil.pngbin0 -> 1497 bytes
-rw-r--r--app/assets/images/emoji/speaker.pngbin0 -> 575 bytes
-rw-r--r--app/assets/images/emoji/speaking_head.pngbin0 -> 531 bytes
-rw-r--r--app/assets/images/emoji/speech_balloon.pngbin0 -> 384 bytes
-rw-r--r--app/assets/images/emoji/speedboat.pngbin0 -> 1255 bytes
-rw-r--r--app/assets/images/emoji/spider.pngbin0 -> 1724 bytes
-rw-r--r--app/assets/images/emoji/spider_web.pngbin0 -> 929 bytes
-rw-r--r--app/assets/images/emoji/spoon.pngbin0 -> 700 bytes
-rw-r--r--app/assets/images/emoji/spy.pngbin0 -> 1650 bytes
-rw-r--r--app/assets/images/emoji/spy_tone1.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/spy_tone2.pngbin0 -> 1632 bytes
-rw-r--r--app/assets/images/emoji/spy_tone3.pngbin0 -> 1645 bytes
-rw-r--r--app/assets/images/emoji/spy_tone4.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/spy_tone5.pngbin0 -> 1639 bytes
-rw-r--r--app/assets/images/emoji/squid.pngbin0 -> 1394 bytes
-rw-r--r--app/assets/images/emoji/stadium.pngbin0 -> 1515 bytes
-rw-r--r--app/assets/images/emoji/star.pngbin0 -> 456 bytes
-rw-r--r--app/assets/images/emoji/star2.pngbin0 -> 732 bytes
-rw-r--r--app/assets/images/emoji/star_and_crescent.pngbin0 -> 490 bytes
-rw-r--r--app/assets/images/emoji/star_of_david.pngbin0 -> 491 bytes
-rw-r--r--app/assets/images/emoji/stars.pngbin0 -> 1048 bytes
-rw-r--r--app/assets/images/emoji/station.pngbin0 -> 1336 bytes
-rw-r--r--app/assets/images/emoji/statue_of_liberty.pngbin0 -> 1145 bytes
-rw-r--r--app/assets/images/emoji/steam_locomotive.pngbin0 -> 1736 bytes
-rw-r--r--app/assets/images/emoji/stew.pngbin0 -> 1960 bytes
-rw-r--r--app/assets/images/emoji/stop_button.pngbin0 -> 385 bytes
-rw-r--r--app/assets/images/emoji/stopwatch.pngbin0 -> 1329 bytes
-rw-r--r--app/assets/images/emoji/straight_ruler.pngbin0 -> 1406 bytes
-rw-r--r--app/assets/images/emoji/strawberry.pngbin0 -> 1206 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue.pngbin0 -> 752 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_closed_eyes.pngbin0 -> 867 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_winking_eye.pngbin0 -> 1061 bytes
-rw-r--r--app/assets/images/emoji/stuffed_flatbread.pngbin0 -> 2160 bytes
-rw-r--r--app/assets/images/emoji/sun_with_face.pngbin0 -> 741 bytes
-rw-r--r--app/assets/images/emoji/sunflower.pngbin0 -> 1915 bytes
-rw-r--r--app/assets/images/emoji/sunglasses.pngbin0 -> 824 bytes
-rw-r--r--app/assets/images/emoji/sunny.pngbin0 -> 746 bytes
-rw-r--r--app/assets/images/emoji/sunrise.pngbin0 -> 812 bytes
-rw-r--r--app/assets/images/emoji/sunrise_over_mountains.pngbin0 -> 1576 bytes
-rw-r--r--app/assets/images/emoji/surfer.pngbin0 -> 1777 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone1.pngbin0 -> 1781 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone2.pngbin0 -> 1769 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone3.pngbin0 -> 1777 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone4.pngbin0 -> 1784 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone5.pngbin0 -> 1782 bytes
-rw-r--r--app/assets/images/emoji/sushi.pngbin0 -> 2101 bytes
-rw-r--r--app/assets/images/emoji/suspension_railway.pngbin0 -> 927 bytes
-rw-r--r--app/assets/images/emoji/sweat.pngbin0 -> 861 bytes
-rw-r--r--app/assets/images/emoji/sweat_drops.pngbin0 -> 549 bytes
-rw-r--r--app/assets/images/emoji/sweat_smile.pngbin0 -> 851 bytes
-rw-r--r--app/assets/images/emoji/sweet_potato.pngbin0 -> 951 bytes
-rw-r--r--app/assets/images/emoji/swimmer.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone1.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone2.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone3.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone4.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone5.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/symbols.pngbin0 -> 746 bytes
-rw-r--r--app/assets/images/emoji/synagogue.pngbin0 -> 1309 bytes
-rw-r--r--app/assets/images/emoji/syringe.pngbin0 -> 737 bytes
-rw-r--r--app/assets/images/emoji/taco.pngbin0 -> 3045 bytes
-rw-r--r--app/assets/images/emoji/tada.pngbin0 -> 1778 bytes
-rw-r--r--app/assets/images/emoji/tanabata_tree.pngbin0 -> 1479 bytes
-rw-r--r--app/assets/images/emoji/tangerine.pngbin0 -> 1184 bytes
-rw-r--r--app/assets/images/emoji/taurus.pngbin0 -> 701 bytes
-rw-r--r--app/assets/images/emoji/taxi.pngbin0 -> 1230 bytes
-rw-r--r--app/assets/images/emoji/tea.pngbin0 -> 1297 bytes
-rw-r--r--app/assets/images/emoji/telephone.pngbin0 -> 1760 bytes
-rw-r--r--app/assets/images/emoji/telephone_receiver.pngbin0 -> 941 bytes
-rw-r--r--app/assets/images/emoji/telescope.pngbin0 -> 1256 bytes
-rw-r--r--app/assets/images/emoji/ten.pngbin0 -> 621 bytes
-rw-r--r--app/assets/images/emoji/tennis.pngbin0 -> 1561 bytes
-rw-r--r--app/assets/images/emoji/tent.pngbin0 -> 1684 bytes
-rw-r--r--app/assets/images/emoji/thermometer.pngbin0 -> 759 bytes
-rw-r--r--app/assets/images/emoji/thermometer_face.pngbin0 -> 1503 bytes
-rw-r--r--app/assets/images/emoji/thinking.pngbin0 -> 1345 bytes
-rw-r--r--app/assets/images/emoji/third_place.pngbin0 -> 1529 bytes
-rw-r--r--app/assets/images/emoji/thought_balloon.pngbin0 -> 489 bytes
-rw-r--r--app/assets/images/emoji/three.pngbin0 -> 602 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone1.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone2.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone3.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone4.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone5.pngbin0 -> 815 bytes
-rw-r--r--app/assets/images/emoji/thumbsup.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone1.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone2.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone3.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone4.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone5.pngbin0 -> 814 bytes
-rw-r--r--app/assets/images/emoji/thunder_cloud_rain.pngbin0 -> 1020 bytes
-rw-r--r--app/assets/images/emoji/ticket.pngbin0 -> 763 bytes
-rw-r--r--app/assets/images/emoji/tickets.pngbin0 -> 1750 bytes
-rw-r--r--app/assets/images/emoji/tiger.pngbin0 -> 2104 bytes
-rw-r--r--app/assets/images/emoji/tiger2.pngbin0 -> 2623 bytes
-rw-r--r--app/assets/images/emoji/timer.pngbin0 -> 1897 bytes
-rw-r--r--app/assets/images/emoji/tired_face.pngbin0 -> 1126 bytes
-rw-r--r--app/assets/images/emoji/tm.pngbin0 -> 300 bytes
-rw-r--r--app/assets/images/emoji/toilet.pngbin0 -> 726 bytes
-rw-r--r--app/assets/images/emoji/tokyo_tower.pngbin0 -> 765 bytes
-rw-r--r--app/assets/images/emoji/tomato.pngbin0 -> 1055 bytes
-rw-r--r--app/assets/images/emoji/tone1.pngbin0 -> 372 bytes
-rw-r--r--app/assets/images/emoji/tone2.pngbin0 -> 372 bytes
-rw-r--r--app/assets/images/emoji/tone3.pngbin0 -> 375 bytes
-rw-r--r--app/assets/images/emoji/tone4.pngbin0 -> 374 bytes
-rw-r--r--app/assets/images/emoji/tone5.pngbin0 -> 374 bytes
-rw-r--r--app/assets/images/emoji/tongue.pngbin0 -> 599 bytes
-rw-r--r--app/assets/images/emoji/tools.pngbin0 -> 1225 bytes
-rw-r--r--app/assets/images/emoji/top.pngbin0 -> 389 bytes
-rw-r--r--app/assets/images/emoji/tophat.pngbin0 -> 845 bytes
-rw-r--r--app/assets/images/emoji/track_next.pngbin0 -> 551 bytes
-rw-r--r--app/assets/images/emoji/track_previous.pngbin0 -> 549 bytes
-rw-r--r--app/assets/images/emoji/trackball.pngbin0 -> 892 bytes
-rw-r--r--app/assets/images/emoji/tractor.pngbin0 -> 1192 bytes
-rw-r--r--app/assets/images/emoji/traffic_light.pngbin0 -> 590 bytes
-rw-r--r--app/assets/images/emoji/train.pngbin0 -> 1031 bytes
-rw-r--r--app/assets/images/emoji/train2.pngbin0 -> 1499 bytes
-rw-r--r--app/assets/images/emoji/tram.pngbin0 -> 1065 bytes
-rw-r--r--app/assets/images/emoji/triangular_flag_on_post.pngbin0 -> 415 bytes
-rw-r--r--app/assets/images/emoji/triangular_ruler.pngbin0 -> 369 bytes
-rw-r--r--app/assets/images/emoji/trident.pngbin0 -> 668 bytes
-rw-r--r--app/assets/images/emoji/triumph.pngbin0 -> 1529 bytes
-rw-r--r--app/assets/images/emoji/trolleybus.pngbin0 -> 1168 bytes
-rw-r--r--app/assets/images/emoji/trophy.pngbin0 -> 863 bytes
-rw-r--r--app/assets/images/emoji/tropical_drink.pngbin0 -> 1428 bytes
-rw-r--r--app/assets/images/emoji/tropical_fish.pngbin0 -> 1676 bytes
-rw-r--r--app/assets/images/emoji/truck.pngbin0 -> 1366 bytes
-rw-r--r--app/assets/images/emoji/trumpet.pngbin0 -> 1281 bytes
-rw-r--r--app/assets/images/emoji/tulip.pngbin0 -> 1065 bytes
-rw-r--r--app/assets/images/emoji/tumbler_glass.pngbin0 -> 2312 bytes
-rw-r--r--app/assets/images/emoji/turkey.pngbin0 -> 1240 bytes
-rw-r--r--app/assets/images/emoji/turtle.pngbin0 -> 1515 bytes
-rw-r--r--app/assets/images/emoji/tv.pngbin0 -> 776 bytes
-rw-r--r--app/assets/images/emoji/twisted_rightwards_arrows.pngbin0 -> 574 bytes
-rw-r--r--app/assets/images/emoji/two.pngbin0 -> 567 bytes
-rw-r--r--app/assets/images/emoji/two_hearts.pngbin0 -> 493 bytes
-rw-r--r--app/assets/images/emoji/two_men_holding_hands.pngbin0 -> 1347 bytes
-rw-r--r--app/assets/images/emoji/two_women_holding_hands.pngbin0 -> 1544 bytes
-rw-r--r--app/assets/images/emoji/u5272.pngbin0 -> 411 bytes
-rw-r--r--app/assets/images/emoji/u5408.pngbin0 -> 484 bytes
-rw-r--r--app/assets/images/emoji/u55b6.pngbin0 -> 460 bytes
-rw-r--r--app/assets/images/emoji/u6307.pngbin0 -> 504 bytes
-rw-r--r--app/assets/images/emoji/u6708.pngbin0 -> 409 bytes
-rw-r--r--app/assets/images/emoji/u6709.pngbin0 -> 434 bytes
-rw-r--r--app/assets/images/emoji/u6e80.pngbin0 -> 564 bytes
-rw-r--r--app/assets/images/emoji/u7121.pngbin0 -> 534 bytes
-rw-r--r--app/assets/images/emoji/u7533.pngbin0 -> 306 bytes
-rw-r--r--app/assets/images/emoji/u7981.pngbin0 -> 584 bytes
-rw-r--r--app/assets/images/emoji/u7a7a.pngbin0 -> 456 bytes
-rw-r--r--app/assets/images/emoji/umbrella.pngbin0 -> 1229 bytes
-rw-r--r--app/assets/images/emoji/umbrella2.pngbin0 -> 897 bytes
-rw-r--r--app/assets/images/emoji/unamused.pngbin0 -> 632 bytes
-rw-r--r--app/assets/images/emoji/underage.pngbin0 -> 863 bytes
-rw-r--r--app/assets/images/emoji/unicorn.pngbin0 -> 2107 bytes
-rw-r--r--app/assets/images/emoji/unlock.pngbin0 -> 856 bytes
-rw-r--r--app/assets/images/emoji/up.pngbin0 -> 405 bytes
-rw-r--r--app/assets/images/emoji/upside_down.pngbin0 -> 602 bytes
-rw-r--r--app/assets/images/emoji/urn.pngbin0 -> 742 bytes
-rw-r--r--app/assets/images/emoji/v.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/v_tone1.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/v_tone2.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/v_tone3.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/v_tone4.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/v_tone5.pngbin0 -> 1009 bytes
-rw-r--r--app/assets/images/emoji/vertical_traffic_light.pngbin0 -> 752 bytes
-rw-r--r--app/assets/images/emoji/vhs.pngbin0 -> 632 bytes
-rw-r--r--app/assets/images/emoji/vibration_mode.pngbin0 -> 683 bytes
-rw-r--r--app/assets/images/emoji/video_camera.pngbin0 -> 1611 bytes
-rw-r--r--app/assets/images/emoji/video_game.pngbin0 -> 765 bytes
-rw-r--r--app/assets/images/emoji/violin.pngbin0 -> 1156 bytes
-rw-r--r--app/assets/images/emoji/virgo.pngbin0 -> 618 bytes
-rw-r--r--app/assets/images/emoji/volcano.pngbin0 -> 1257 bytes
-rw-r--r--app/assets/images/emoji/volleyball.pngbin0 -> 1202 bytes
-rw-r--r--app/assets/images/emoji/vs.pngbin0 -> 604 bytes
-rw-r--r--app/assets/images/emoji/vulcan.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone1.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone2.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone3.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone4.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone5.pngbin0 -> 1083 bytes
-rw-r--r--app/assets/images/emoji/walking.pngbin0 -> 1082 bytes
-rw-r--r--app/assets/images/emoji/walking_tone1.pngbin0 -> 1084 bytes
-rw-r--r--app/assets/images/emoji/walking_tone2.pngbin0 -> 1084 bytes
-rw-r--r--app/assets/images/emoji/walking_tone3.pngbin0 -> 1066 bytes
-rw-r--r--app/assets/images/emoji/walking_tone4.pngbin0 -> 1075 bytes
-rw-r--r--app/assets/images/emoji/walking_tone5.pngbin0 -> 1065 bytes
-rw-r--r--app/assets/images/emoji/waning_crescent_moon.pngbin0 -> 1213 bytes
-rw-r--r--app/assets/images/emoji/waning_gibbous_moon.pngbin0 -> 1208 bytes
-rw-r--r--app/assets/images/emoji/warning.pngbin0 -> 565 bytes
-rw-r--r--app/assets/images/emoji/wastebasket.pngbin0 -> 2414 bytes
-rw-r--r--app/assets/images/emoji/watch.pngbin0 -> 785 bytes
-rw-r--r--app/assets/images/emoji/water_buffalo.pngbin0 -> 1536 bytes
-rw-r--r--app/assets/images/emoji/water_polo.pngbin0 -> 1755 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone1.pngbin0 -> 1758 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone2.pngbin0 -> 1756 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone3.pngbin0 -> 1760 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone4.pngbin0 -> 1749 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone5.pngbin0 -> 1748 bytes
-rw-r--r--app/assets/images/emoji/watermelon.pngbin0 -> 1275 bytes
-rw-r--r--app/assets/images/emoji/wave.pngbin0 -> 1300 bytes
-rw-r--r--app/assets/images/emoji/wave_tone1.pngbin0 -> 1300 bytes
-rw-r--r--app/assets/images/emoji/wave_tone2.pngbin0 -> 1300 bytes
-rw-r--r--app/assets/images/emoji/wave_tone3.pngbin0 -> 1295 bytes
-rw-r--r--app/assets/images/emoji/wave_tone4.pngbin0 -> 1300 bytes
-rw-r--r--app/assets/images/emoji/wave_tone5.pngbin0 -> 1300 bytes
-rw-r--r--app/assets/images/emoji/wavy_dash.pngbin0 -> 359 bytes
-rw-r--r--app/assets/images/emoji/waxing_crescent_moon.pngbin0 -> 1199 bytes
-rw-r--r--app/assets/images/emoji/waxing_gibbous_moon.pngbin0 -> 1229 bytes
-rw-r--r--app/assets/images/emoji/wc.pngbin0 -> 752 bytes
-rw-r--r--app/assets/images/emoji/weary.pngbin0 -> 871 bytes
-rw-r--r--app/assets/images/emoji/wedding.pngbin0 -> 1260 bytes
-rw-r--r--app/assets/images/emoji/whale.pngbin0 -> 1572 bytes
-rw-r--r--app/assets/images/emoji/whale2.pngbin0 -> 1196 bytes
-rw-r--r--app/assets/images/emoji/wheel_of_dharma.pngbin0 -> 666 bytes
-rw-r--r--app/assets/images/emoji/wheelchair.pngbin0 -> 683 bytes
-rw-r--r--app/assets/images/emoji/white_check_mark.pngbin0 -> 547 bytes
-rw-r--r--app/assets/images/emoji/white_circle.pngbin0 -> 351 bytes
-rw-r--r--app/assets/images/emoji/white_flower.pngbin0 -> 941 bytes
-rw-r--r--app/assets/images/emoji/white_large_square.pngbin0 -> 110 bytes
-rw-r--r--app/assets/images/emoji/white_medium_small_square.pngbin0 -> 110 bytes
-rw-r--r--app/assets/images/emoji/white_medium_square.pngbin0 -> 108 bytes
-rw-r--r--app/assets/images/emoji/white_small_square.pngbin0 -> 108 bytes
-rw-r--r--app/assets/images/emoji/white_square_button.pngbin0 -> 122 bytes
-rw-r--r--app/assets/images/emoji/white_sun_cloud.pngbin0 -> 968 bytes
-rw-r--r--app/assets/images/emoji/white_sun_rain_cloud.pngbin0 -> 1161 bytes
-rw-r--r--app/assets/images/emoji/white_sun_small_cloud.pngbin0 -> 989 bytes
-rw-r--r--app/assets/images/emoji/wilted_rose.pngbin0 -> 1349 bytes
-rw-r--r--app/assets/images/emoji/wind_blowing_face.pngbin0 -> 1827 bytes
-rw-r--r--app/assets/images/emoji/wind_chime.pngbin0 -> 1046 bytes
-rw-r--r--app/assets/images/emoji/wine_glass.pngbin0 -> 655 bytes
-rw-r--r--app/assets/images/emoji/wink.pngbin0 -> 746 bytes
-rw-r--r--app/assets/images/emoji/wolf.pngbin0 -> 1528 bytes
-rw-r--r--app/assets/images/emoji/woman.pngbin0 -> 1212 bytes
-rw-r--r--app/assets/images/emoji/woman_tone1.pngbin0 -> 1212 bytes
-rw-r--r--app/assets/images/emoji/woman_tone2.pngbin0 -> 1212 bytes
-rw-r--r--app/assets/images/emoji/woman_tone3.pngbin0 -> 1202 bytes
-rw-r--r--app/assets/images/emoji/woman_tone4.pngbin0 -> 1195 bytes
-rw-r--r--app/assets/images/emoji/woman_tone5.pngbin0 -> 1202 bytes
-rw-r--r--app/assets/images/emoji/womans_clothes.pngbin0 -> 1042 bytes
-rw-r--r--app/assets/images/emoji/womans_hat.pngbin0 -> 1553 bytes
-rw-r--r--app/assets/images/emoji/womens.pngbin0 -> 577 bytes
-rw-r--r--app/assets/images/emoji/worried.pngbin0 -> 715 bytes
-rw-r--r--app/assets/images/emoji/wrench.pngbin0 -> 418 bytes
-rw-r--r--app/assets/images/emoji/wrestlers.pngbin0 -> 2556 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone1.pngbin0 -> 2563 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone2.pngbin0 -> 2553 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone3.pngbin0 -> 2541 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone4.pngbin0 -> 2553 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone5.pngbin0 -> 2542 bytes
-rw-r--r--app/assets/images/emoji/writing_hand.pngbin0 -> 1001 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone1.pngbin0 -> 988 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone2.pngbin0 -> 987 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone3.pngbin0 -> 977 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone4.pngbin0 -> 973 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone5.pngbin0 -> 970 bytes
-rw-r--r--app/assets/images/emoji/x.pngbin0 -> 298 bytes
-rw-r--r--app/assets/images/emoji/yellow_heart.pngbin0 -> 435 bytes
-rw-r--r--app/assets/images/emoji/yen.pngbin0 -> 421 bytes
-rw-r--r--app/assets/images/emoji/yin_yang.pngbin0 -> 776 bytes
-rw-r--r--app/assets/images/emoji/yum.pngbin0 -> 896 bytes
-rw-r--r--app/assets/images/emoji/zap.pngbin0 -> 413 bytes
-rw-r--r--app/assets/images/emoji/zero.pngbin0 -> 560 bytes
-rw-r--r--app/assets/images/emoji/zipper_mouth.pngbin0 -> 722 bytes
-rw-r--r--app/assets/images/emoji/zzz.pngbin0 -> 540 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin2652225 -> 2976505 bytes
-rw-r--r--app/assets/images/icon-merge-request-unmerged.svg1
-rw-r--r--app/assets/images/mailers/gitlab_footer_logo.gifbin0 -> 3654 bytes
-rw-r--r--app/assets/images/mailers/gitlab_header_logo.gifbin0 -> 3040 bytes
-rw-r--r--app/assets/javascripts/abuse_reports.js37
-rw-r--r--app/assets/javascripts/abuse_reports.js.es640
-rw-r--r--app/assets/javascripts/activities.js36
-rw-r--r--app/assets/javascripts/activities.js.es637
-rw-r--r--app/assets/javascripts/admin.js120
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js35
-rw-r--r--app/assets/javascripts/api.js290
-rw-r--r--app/assets/javascripts/application.js246
-rw-r--r--app/assets/javascripts/aside.js45
-rw-r--r--app/assets/javascripts/autosave.js109
-rw-r--r--app/assets/javascripts/awards_handler.js876
-rw-r--r--app/assets/javascripts/behaviors/bind_in_out.js47
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js116
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js11
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js121
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js50
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js161
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js2
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js6
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js (renamed from app/assets/javascripts/blob/blob_ci_yaml.js.es6)0
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js (renamed from app/assets/javascripts/blob/blob_dockerfile_selector.js.es6)0
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js (renamed from app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6)0
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js (renamed from app/assets/javascripts/blob/blob_license_selectors.js.es6)0
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js35
-rw-r--r--app/assets/javascripts/blob/create_branch_dropdown.js88
-rw-r--r--app/assets/javascripts/blob/target_branch_dropdown.js152
-rw-r--r--app/assets/javascripts/blob/template_selector.js (renamed from app/assets/javascripts/blob/template_selector.js.es6)0
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js167
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es6111
-rw-r--r--app/assets/javascripts/boards/components/board.js106
-rw-r--r--app/assets/javascripts/boards/components/board.js.es6105
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js84
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es653
-rw-r--r--app/assets/javascripts/boards/components/board_card.js70
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es661
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js (renamed from app/assets/javascripts/boards/components/board_delete.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/board_list.js131
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es6120
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js92
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es664
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js (renamed from app/assets/javascripts/boards/components/board_sidebar.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js114
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js.es6111
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js70
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js.es670
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js24
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js.es649
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/label.js.es654
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/milestone.js.es655
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/user.js.es696
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js (renamed from app/assets/javascripts/boards/components/modal/footer.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js82
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js.es690
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js159
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js.es6163
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js (renamed from app/assets/javascripts/boards/components/modal/list.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js (renamed from app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js47
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js.es647
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js (renamed from app/assets/javascripts/boards/components/new_list_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js (renamed from app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6)0
-rw-r--r--app/assets/javascripts/boards/eventhub.js3
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js41
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js (renamed from app/assets/javascripts/boards/filters/due_date_filters.js.es6)0
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js (renamed from app/assets/javascripts/boards/mixins/modal_mixins.js.es6)0
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js (renamed from app/assets/javascripts/boards/mixins/sortable_default_options.js.es6)0
-rw-r--r--app/assets/javascripts/boards/models/issue.js75
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es678
-rw-r--r--app/assets/javascripts/boards/models/label.js (renamed from app/assets/javascripts/boards/models/label.js.es6)0
-rw-r--r--app/assets/javascripts/boards/models/list.js172
-rw-r--r--app/assets/javascripts/boards/models/list.js.es6152
-rw-r--r--app/assets/javascripts/boards/models/milestone.js (renamed from app/assets/javascripts/boards/models/milestone.js.es6)0
-rw-r--r--app/assets/javascripts/boards/models/user.js (renamed from app/assets/javascripts/boards/models/user.js.es6)0
-rw-r--r--app/assets/javascripts/boards/services/board_service.js97
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es695
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js126
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es6120
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js99
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js.es6107
-rw-r--r--app/assets/javascripts/boards/utils/query_data.js21
-rw-r--r--app/assets/javascripts/breakpoints.js108
-rw-r--r--app/assets/javascripts/broadcast_message.js61
-rw-r--r--app/assets/javascripts/build.js523
-rw-r--r--app/assets/javascripts/build_artifacts.js43
-rw-r--r--app/assets/javascripts/build_variables.js (renamed from app/assets/javascripts/build_variables.js.es6)0
-rw-r--r--app/assets/javascripts/ci_lint_editor.js17
-rw-r--r--app/assets/javascripts/ci_lint_editor.js.es618
-rw-r--r--app/assets/javascripts/commit.js18
-rw-r--r--app/assets/javascripts/commit/image_file.js81
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js30
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es629
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js.es644
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es648
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js110
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es6110
-rw-r--r--app/assets/javascripts/commits.js118
-rw-r--r--app/assets/javascripts/commons/bootstrap.js16
-rw-r--r--app/assets/javascripts/commons/index.js3
-rw-r--r--app/assets/javascripts/commons/jquery.js11
-rw-r--r--app/assets/javascripts/commons/polyfills.js10
-rw-r--r--app/assets/javascripts/commons/polyfills/custom_event.js9
-rw-r--r--app/assets/javascripts/commons/polyfills/element.js20
-rw-r--r--app/assets/javascripts/compare.js165
-rw-r--r--app/assets/javascripts/compare_autocomplete.js68
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es669
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js57
-rw-r--r--app/assets/javascripts/copy_as_gfm.js402
-rw-r--r--app/assets/javascripts/copy_as_gfm.js.es6358
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js93
-rw-r--r--app/assets/javascripts/create_label.js127
-rw-r--r--app/assets/javascripts/create_label.js.es6132
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js (renamed from app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js (renamed from app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js56
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es650
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js (renamed from app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js (renamed from app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js48
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js49
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js (renamed from app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js135
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6128
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js (renamed from app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js104
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6101
-rw-r--r--app/assets/javascripts/cycle_analytics/default_event_objects.js (renamed from app/assets/javascripts/cycle_analytics/default_event_objects.js.es6)0
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_branch.svg1
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg1
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_commit.svg1
-rw-r--r--app/assets/javascripts/diff.js128
-rw-r--r--app/assets/javascripts/diff.js.es6126
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js (renamed from app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js156
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js (renamed from app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js29
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js120
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es6108
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js (renamed from app/assets/javascripts/diff_notes/components/resolve_count.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js (renamed from app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js68
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es653
-rw-r--r--app/assets/javascripts/diff_notes/icons/collapse_icon.svg1
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js (renamed from app/assets/javascripts/diff_notes/mixins/discussion.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js96
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es696
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js16
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es613
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js (renamed from app/assets/javascripts/diff_notes/services/resolve.js.es6)0
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js57
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es657
-rw-r--r--app/assets/javascripts/dispatcher.js448
-rw-r--r--app/assets/javascripts/dispatcher.js.es6386
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js9
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/dropzone_input.js405
-rw-r--r--app/assets/javascripts/due_date_select.js203
-rw-r--r--app/assets/javascripts/due_date_select.js.es6204
-rw-r--r--app/assets/javascripts/environments/components/environment.js192
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6193
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js71
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es643
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js22
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es619
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js514
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es6549
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js67
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es630
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js56
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es624
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js27
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es625
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js60
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js.es674
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js13
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es613
-rw-r--r--app/assets/javascripts/environments/event_hub.js3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js13
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js.es613
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js178
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js.es6182
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js19
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es613
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js88
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es690
-rw-r--r--app/assets/javascripts/extensions/array.js11
-rw-r--r--app/assets/javascripts/extensions/array.js.es627
-rw-r--r--app/assets/javascripts/extensions/custom_event.js.es612
-rw-r--r--app/assets/javascripts/extensions/element.js.es620
-rw-r--r--app/assets/javascripts/extensions/jquery.js16
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/files_comment_button.js234
-rw-r--r--app/assets/javascripts/filterable_list.js46
-rw-r--r--app/assets/javascripts/filtered_search/container.js14
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js83
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es664
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js (renamed from app/assets/javascripts/filtered_search/dropdown_non_user.js.es6)0
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js65
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es660
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js181
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es6126
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js124
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6119
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js191
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6210
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js423
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6231
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js100
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es696
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js (renamed from app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6)0
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js202
-rw-r--r--app/assets/javascripts/flash.js73
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js390
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6383
-rw-r--r--app/assets/javascripts/gl_dropdown.js1488
-rw-r--r--app/assets/javascripts/gl_field_error.js162
-rw-r--r--app/assets/javascripts/gl_field_error.js.es6164
-rw-r--r--app/assets/javascripts/gl_field_errors.js47
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es648
-rw-r--r--app/assets/javascripts/gl_form.js90
-rw-r--r--app/assets/javascripts/gl_form.js.es692
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js9
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js18
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js201
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js542
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js265
-rw-r--r--app/assets/javascripts/group_avatar.js35
-rw-r--r--app/assets/javascripts/group_label_subscription.js52
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/group_name.js40
-rw-r--r--app/assets/javascripts/groups_list.js18
-rw-r--r--app/assets/javascripts/groups_select.js124
-rw-r--r--app/assets/javascripts/header.js15
-rw-r--r--app/assets/javascripts/issuable.js188
-rw-r--r--app/assets/javascripts/issuable.js.es6188
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js (renamed from app/assets/javascripts/issuable/issuable_bundle.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js42
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es641
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js (renamed from app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js (renamed from app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js (renamed from app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js (renamed from app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js (renamed from app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6)0
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js117
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6119
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js65
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es662
-rw-r--r--app/assets/javascripts/issue.js228
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js (renamed from app/assets/javascripts/issues_bulk_assignment.js.es6)0
-rw-r--r--app/assets/javascripts/label_manager.js (renamed from app/assets/javascripts/label_manager.js.es6)0
-rw-r--r--app/assets/javascripts/labels_select.js20
-rw-r--r--app/assets/javascripts/lib/chart.js3
-rw-r--r--app/assets/javascripts/lib/cropper.js7
-rw-r--r--app/assets/javascripts/lib/d3.js3
-rw-r--r--app/assets/javascripts/lib/raphael.js9
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js (renamed from app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6)0
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js342
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es6300
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js (renamed from app/assets/javascripts/lib/utils/datetime_utility.js.es6)0
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js10
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js (renamed from app/assets/javascripts/lib/utils/pretty_time.js.es6)0
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js18
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js93
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.es686
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js12
-rw-r--r--app/assets/javascripts/main.js384
-rw-r--r--app/assets/javascripts/member_expiration_date.js (renamed from app/assets/javascripts/member_expiration_date.js.es6)0
-rw-r--r--app/assets/javascripts/members.js (renamed from app/assets/javascripts/members.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js (renamed from app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js (renamed from app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js (renamed from app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js (renamed from app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js (renamed from app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js (renamed from app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js (renamed from app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6)0
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js (renamed from app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6)0
-rw-r--r--app/assets/javascripts/merge_request_tabs.js (renamed from app/assets/javascripts/merge_request_tabs.js.es6)0
-rw-r--r--app/assets/javascripts/merge_request_widget.js296
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6285
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js53
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es653
-rw-r--r--app/assets/javascripts/milestone.js1
-rw-r--r--app/assets/javascripts/milestone_select.js27
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js110
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es695
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js335
-rw-r--r--app/assets/javascripts/network/branch_graph.js703
-rw-r--r--app/assets/javascripts/network/network.js33
-rw-r--r--app/assets/javascripts/network/network_bundle.js27
-rw-r--r--app/assets/javascripts/network/raphael.js74
-rw-r--r--app/assets/javascripts/new_branch_form.js32
-rw-r--r--app/assets/javascripts/new_commit_form.js12
-rw-r--r--app/assets/javascripts/notes.js152
-rw-r--r--app/assets/javascripts/pager.js77
-rw-r--r--app/assets/javascripts/pager.js.es673
-rw-r--r--app/assets/javascripts/pipelines.js (renamed from app/assets/javascripts/pipelines.js.es6)0
-rw-r--r--app/assets/javascripts/profile/gl_crop.js173
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es6171
-rw-r--r--app/assets/javascripts/profile/profile.js99
-rw-r--r--app/assets/javascripts/profile/profile.js.es699
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js5
-rw-r--r--app/assets/javascripts/project.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js (renamed from app/assets/javascripts/project_label_subscription.js.es6)0
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/project_variables.js (renamed from app/assets/javascripts/project_variables.js.es6)0
-rw-r--r--app/assets/javascripts/projects_list.js64
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js (renamed from app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js (renamed from app/assets/javascripts/protected_branches/protected_branch_create.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js (renamed from app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js69
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es666
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js (renamed from app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6)0
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js8
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js (renamed from app/assets/javascripts/search_autocomplete.js.es6)0
-rw-r--r--app/assets/javascripts/shortcuts_blob.js (renamed from app/assets/javascripts/shortcuts_blob.js.es6)0
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js (renamed from app/assets/javascripts/signin_tabs_memoizer.js.es6)0
-rw-r--r--app/assets/javascripts/smart_interval.js (renamed from app/assets/javascripts/smart_interval.js.es6)0
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js4
-rw-r--r--app/assets/javascripts/snippets_list.js (renamed from app/assets/javascripts/snippets_list.js.es6)0
-rw-r--r--app/assets/javascripts/subbable_resource.js (renamed from app/assets/javascripts/subbable_resource.js.es6)0
-rw-r--r--app/assets/javascripts/subscription.js (renamed from app/assets/javascripts/subscription.js.es6)0
-rw-r--r--app/assets/javascripts/task_list.js12
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js (renamed from app/assets/javascripts/templates/issuable_template_selector.js.es6)0
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js (renamed from app/assets/javascripts/templates/issuable_template_selectors.js.es6)0
-rw-r--r--app/assets/javascripts/terminal/terminal.js (renamed from app/assets/javascripts/terminal/terminal.js.es6)0
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js (renamed from app/assets/javascripts/terminal/terminal_bundle.js.es6)0
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js29
-rw-r--r--app/assets/javascripts/todos.js163
-rw-r--r--app/assets/javascripts/todos.js.es6146
-rw-r--r--app/assets/javascripts/u2f/authenticate.js (renamed from app/assets/javascripts/u2f/authenticate.js.es6)0
-rw-r--r--app/assets/javascripts/user.js (renamed from app/assets/javascripts/user.js.es6)0
-rw-r--r--app/assets/javascripts/user_callout.js60
-rw-r--r--app/assets/javascripts/user_tabs.js (renamed from app/assets/javascripts/user_tabs.js.es6)0
-rw-r--r--app/assets/javascripts/username_validator.js (renamed from app/assets/javascripts/username_validator.js.es6)0
-rw-r--r--app/assets/javascripts/users/calendar.js3
-rw-r--r--app/assets/javascripts/users/users_bundle.js4
-rw-r--r--app/assets/javascripts/users_select.js32
-rw-r--r--app/assets/javascripts/version_check_image.js10
-rw-r--r--app/assets/javascripts/version_check_image.js.es610
-rw-r--r--app/assets/javascripts/visibility_select.js (renamed from app/assets/javascripts/visibility_select.js.es6)0
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js92
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js32
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/stage.js116
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/status.js60
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js28
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es636
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6105
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es663
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js121
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es693
-rw-r--r--app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js44
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es6103
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js.es634
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es631
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js61
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es676
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js (renamed from app/assets/javascripts/vue_realtime_listener/index.js.es6)0
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js157
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es6164
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js48
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es661
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js228
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6234
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js135
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es6150
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js21
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es623
-rw-r--r--app/assets/javascripts/wikis.js (renamed from app/assets/javascripts/wikis.js.es6)0
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss22
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss44
-rw-r--r--app/assets/stylesheets/framework/emoji-sprites.scss1811
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1814
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss215
-rw-r--r--app/assets/stylesheets/framework/header.scss65
-rw-r--r--app/assets/stylesheets/framework/highlight.scss9
-rw-r--r--app/assets/stylesheets/framework/jquery.scss11
-rw-r--r--app/assets/stylesheets/framework/layout.scss13
-rw-r--r--app/assets/stylesheets/framework/lists.scss48
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss7
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/mobile.scss3
-rw-r--r--app/assets/stylesheets/framework/nav.scss13
-rw-r--r--app/assets/stylesheets/framework/panels.scss8
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss30
-rw-r--r--app/assets/stylesheets/framework/variables.scss11
-rw-r--r--app/assets/stylesheets/framework/zen.scss3
-rw-r--r--app/assets/stylesheets/highlight/dark.scss30
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss30
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss30
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss31
-rw-r--r--app/assets/stylesheets/highlight/white.scss29
-rw-r--r--app/assets/stylesheets/pages/boards.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss64
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss10
-rw-r--r--app/assets/stylesheets/pages/diff.scss115
-rw-r--r--app/assets/stylesheets/pages/environments.scss249
-rw-r--r--app/assets/stylesheets/pages/events.scss5
-rw-r--r--app/assets/stylesheets/pages/groups.scss16
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss89
-rw-r--r--app/assets/stylesheets/pages/note_form.scss33
-rw-r--r--app/assets/stylesheets/pages/notes.scss86
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss222
-rw-r--r--app/assets/stylesheets/pages/profile.scss39
-rw-r--r--app/assets/stylesheets/pages/projects.scss28
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/assets/stylesheets/pages/settings.scss11
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss12
-rw-r--r--app/assets/stylesheets/pages/todos.scss10
-rw-r--r--app/assets/stylesheets/pages/tree.scss36
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/health_check_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb53
-rw-r--r--app/controllers/admin/projects_controller.rb9
-rw-r--r--app/controllers/admin/system_info_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb17
-rw-r--r--app/controllers/application_controller.rb35
-rw-r--r--app/controllers/autocomplete_controller.rb3
-rw-r--r--app/controllers/ci/projects_controller.rb47
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb7
-rw-r--r--app/controllers/concerns/creates_commit.rb51
-rw-r--r--app/controllers/concerns/filter_projects.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb17
-rw-r--r--app/controllers/concerns/issuable_collections.rb20
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-rw-r--r--app/controllers/concerns/repository_settings_redirect.rb7
-rw-r--r--app/controllers/concerns/service_params.rb6
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/dashboard/groups_controller.rb14
-rw-r--r--app/controllers/dashboard/milestones_controller.rb1
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb12
-rw-r--r--app/controllers/emojis_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb11
-rw-r--r--app/controllers/explore/projects_controller.rb14
-rw-r--r--app/controllers/groups/milestones_controller.rb1
-rw-r--r--app/controllers/groups_controller.rb16
-rw-r--r--app/controllers/jwt_controller.rb8
-rw-r--r--app/controllers/oauth/authorizations_controller.rb44
-rw-r--r--app/controllers/profiles/keys_controller.rb5
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb17
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb11
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb6
-rw-r--r--app/controllers/projects/blame_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb15
-rw-r--r--app/controllers/projects/boards/issues_controller.rb11
-rw-r--r--app/controllers/projects/branches_controller.rb53
-rw-r--r--app/controllers/projects/commit_controller.rb32
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb30
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/git_http_client_controller.rb11
-rw-r--r--app/controllers/projects/graphs_controller.rb31
-rw-r--r--app/controllers/projects/issues_controller.rb46
-rw-r--r--app/controllers/projects/merge_requests_controller.rb71
-rw-r--r--app/controllers/projects/notes_controller.rb18
-rw-r--r--app/controllers/projects/pipelines_controller.rb23
-rw-r--r--app/controllers/projects/protected_branches_controller.rb36
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb3
-rw-r--r--app/controllers/projects/settings/members_controller.rb37
-rw-r--r--app/controllers/projects/settings/repository_controller.rb50
-rw-r--r--app/controllers/projects/tags_controller.rb26
-rw-r--r--app/controllers/projects/triggers_controller.rb59
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb10
-rw-r--r--app/controllers/root_controller.rb37
-rw-r--r--app/controllers/sessions_controller.rb11
-rw-r--r--app/controllers/snippets_controller.rb5
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/finders/group_members_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb43
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/members_finder.rb40
-rw-r--r--app/finders/merge_requests_finder.rb6
-rw-r--r--app/finders/notes_finder.rb11
-rw-r--r--app/finders/personal_access_tokens_finder.rb45
-rw-r--r--app/finders/pipelines_finder.rb6
-rw-r--r--app/finders/todos_finder.rb16
-rw-r--r--app/helpers/application_helper.rb13
-rw-r--r--app/helpers/application_settings_helper.rb29
-rw-r--r--app/helpers/blob_helper.rb35
-rw-r--r--app/helpers/builds_helper.rb9
-rw-r--r--app/helpers/button_helper.rb4
-rw-r--r--app/helpers/ci_status_helper.rb22
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/emails_helper.rb19
-rw-r--r--app/helpers/emoji_helper.rb5
-rw-r--r--app/helpers/events_helper.rb7
-rw-r--r--app/helpers/explore_helper.rb23
-rw-r--r--app/helpers/gitlab_markdown_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb38
-rw-r--r--app/helpers/issues_helper.rb42
-rw-r--r--app/helpers/javascript_helper.rb1
-rw-r--r--app/helpers/mattermost_helper.rb6
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb11
-rw-r--r--app/helpers/namespaces_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb18
-rw-r--r--app/helpers/rss_helper.rb5
-rw-r--r--app/helpers/sorting_helper.rb13
-rw-r--r--app/helpers/submodule_helper.rb8
-rw-r--r--app/helpers/tab_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb15
-rw-r--r--app/helpers/triggers_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb10
-rw-r--r--app/mailers/emails/builds.rb30
-rw-r--r--app/mailers/emails/pipelines.rb4
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/repository_check_mailer.rb11
-rw-r--r--app/models/ability.rb7
-rw-r--r--app/models/appearance.rb1
-rw-r--r--app/models/application_setting.rb60
-rw-r--r--app/models/blob.rb8
-rw-r--r--app/models/chat_team.rb6
-rw-r--r--app/models/ci/build.rb121
-rw-r--r--app/models/ci/pipeline.rb42
-rw-r--r--app/models/ci/pipeline_status.rb86
-rw-r--r--app/models/ci/runner.rb21
-rw-r--r--app/models/ci/runner_project.rb6
-rw-r--r--app/models/ci/stage.rb6
-rw-r--r--app/models/ci/trigger.rb17
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb30
-rw-r--r--app/models/commit_status.rb27
-rw-r--r--app/models/concerns/awardable.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb11
-rw-r--r--app/models/concerns/case_sensitivity.rb11
-rw-r--r--app/models/concerns/has_status.rb33
-rw-r--r--app/models/concerns/issuable.rb70
-rw-r--r--app/models/concerns/reactive_service.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb139
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/uniquify.rb30
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/external_issue.rb7
-rw-r--r--app/models/global_milestone.rb22
-rw-r--r--app/models/group.rb13
-rw-r--r--app/models/guest.rb2
-rw-r--r--app/models/issue.rb26
-rw-r--r--app/models/label.rb6
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/group_member.rb4
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb39
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/merge_requests_closing_issues.rb8
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/namespace.rb11
-rw-r--r--app/models/network/graph.rb11
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/oauth_access_grant.rb4
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/personal_access_token.rb26
-rw-r--r--app/models/project.rb87
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_group_link.rb11
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/builds_email_service.rb102
-rw-r--r--app/models/project_services/chat_message/build_message.rb102
-rw-r--r--app/models/project_services/chat_message/issue_message.rb3
-rw-r--r--app/models/project_services/chat_notification_service.rb18
-rw-r--r--app/models/project_services/drone_ci_service.rb6
-rw-r--r--app/models/project_services/hipchat_service.rb49
-rw-r--r--app/models/project_services/irker_service.rb3
-rw-r--r--app/models/project_services/issue_tracker_service.rb11
-rw-r--r--app/models/project_services/kubernetes_service.rb25
-rw-r--r--app/models/project_services/mattermost_service.rb15
-rw-r--r--app/models/project_services/mock_ci_service.rb82
-rw-r--r--app/models/project_services/monitoring_service.rb16
-rw-r--r--app/models/project_services/pivotaltracker_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb93
-rw-r--r--app/models/project_services/pushover_service.rb45
-rw-r--r--app/models/project_services/slack_service.rb15
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_wiki.rb22
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/repository.rb282
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/todo.rb10
-rw-r--r--app/models/upload.rb63
-rw-r--r--app/models/user.rb100
-rw-r--r--app/models/wiki_page.rb14
-rw-r--r--app/policies/base_policy.rb13
-rw-r--r--app/policies/ci/trigger_policy.rb13
-rw-r--r--app/policies/global_policy.rb7
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/policies/user_policy.rb8
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb60
-rw-r--r--app/serializers/build_entity.rb2
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb2
-rw-r--r--app/services/access_token_validation_service.rb8
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/base_service.rb4
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/boards/issues/move_service.rb28
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb4
-rw-r--r--app/services/ci/create_pipeline_service.rb3
-rw-r--r--app/services/ci/create_trigger_request_service.rb2
-rw-r--r--app/services/ci/image_for_build_service.rb25
-rw-r--r--app/services/ci/process_pipeline_service.rb6
-rw-r--r--app/services/ci/register_build_service.rb73
-rw-r--r--app/services/ci/register_job_service.rb85
-rw-r--r--app/services/ci/retry_build_service.rb18
-rw-r--r--app/services/ci/retry_pipeline_service.rb6
-rw-r--r--app/services/commits/change_service.rb56
-rw-r--r--app/services/concerns/issues/resolve_discussions.rb32
-rw-r--r--app/services/create_branch_service.rb14
-rw-r--r--app/services/files/base_service.rb16
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb7
-rw-r--r--app/services/files/destroy_service.rb2
-rw-r--r--app/services/files/multi_service.rb35
-rw-r--r--app/services/files/update_service.rb6
-rw-r--r--app/services/git_operation_service.rb37
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/groups/create_service.rb15
-rw-r--r--app/services/groups/destroy_service.rb3
-rw-r--r--app/services/issuable_base_service.rb11
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/build_service.rb34
-rw-r--r--app/services/issues/create_service.rb21
-rw-r--r--app/services/issues/move_service.rb2
-rw-r--r--app/services/issues/update_service.rb20
-rw-r--r--app/services/mattermost/create_team_service.rb14
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb7
-rw-r--r--app/services/merge_requests/merge_service.rb20
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb24
-rw-r--r--app/services/merge_requests/refresh_service.rb6
-rw-r--r--app/services/merge_requests/resolve_service.rb3
-rw-r--r--app/services/notes/create_service.rb10
-rw-r--r--app/services/notes/slash_commands_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb293
-rw-r--r--app/services/notification_service.rb330
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb4
-rw-r--r--app/services/projects/download_service.rb4
-rw-r--r--app/services/projects/import_service.rb5
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb4
-rw-r--r--app/services/protected_branches/api_update_service.rb8
-rw-r--r--app/services/slash_commands/interpret_service.rb19
-rw-r--r--app/services/system_hooks_service.rb17
-rw-r--r--app/services/system_note_service.rb22
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/services/todo_service.rb14
-rw-r--r--app/services/users/destroy_service.rb21
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb20
-rw-r--r--app/uploaders/artifact_uploader.rb4
-rw-r--r--app/uploaders/attachment_uploader.rb3
-rw-r--r--app/uploaders/avatar_uploader.rb3
-rw-r--r--app/uploaders/file_uploader.rb57
-rw-r--r--app/uploaders/gitlab_uploader.rb25
-rw-r--r--app/uploaders/records_uploads.rb34
-rw-r--r--app/uploaders/uploader_helper.rb12
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/duration_validator.rb17
-rw-r--r--app/validators/importable_url_validator.rb11
-rw-r--r--app/validators/namespace_validator.rb18
-rw-r--r--app/validators/project_path_validator.rb6
-rw-r--r--app/views/admin/abuse_reports/index.html.haml2
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml37
-rw-r--r--app/views/admin/applications/_form.html.haml2
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml8
-rw-r--r--app/views/admin/projects/_projects.html.haml32
-rw-r--r--app/views/admin/projects/index.html.haml85
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/admin/users/_head.html.haml6
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml5
-rw-r--r--app/views/award_emoji/_awards_block.html.haml2
-rw-r--r--app/views/dashboard/_activities.html.haml7
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/dashboard/_projects_head.html.haml3
-rw-r--r--app/views/dashboard/activity.html.haml3
-rw-r--r--app/views/dashboard/groups/_groups.html.haml6
-rw-r--r--app/views/dashboard/groups/index.html.haml7
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/issues.html.haml10
-rw-r--r--app/views/dashboard/milestones/index.html.haml10
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.html.haml8
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml9
-rw-r--r--app/views/dashboard/todos/index.html.haml33
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml4
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_new_issue_for_all_discussions.html.haml6
-rw-r--r--app/views/discussions/_new_issue_for_discussion.html.haml8
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/emojis/index.html.haml11
-rw-r--r--app/views/events/_event.atom.builder4
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/explore/groups/_groups.html.haml6
-rw-r--r--app/views/explore/groups/_nav.html.haml8
-rw-r--r--app/views/explore/groups/index.html.haml42
-rw-r--r--app/views/explore/projects/_nav.html.haml27
-rw-r--r--app/views/explore/projects/index.html.haml7
-rw-r--r--app/views/groups/_activities.html.haml7
-rw-r--r--app/views/groups/_create_chat_team.html.haml16
-rw-r--r--app/views/groups/_head.html.haml14
-rw-r--r--app/views/groups/_head_issues.html.haml19
-rw-r--r--app/views/groups/_settings_head.html.haml14
-rw-r--r--app/views/groups/activity.html.haml6
-rw-r--r--app/views/groups/edit.html.haml1
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/issues.html.haml17
-rw-r--r--app/views/groups/labels/index.html.haml1
-rw-r--r--app/views/groups/milestones/index.html.haml3
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml7
-rw-r--r--app/views/groups/subgroups.html.haml1
-rw-r--r--app/views/help/_shortcuts.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml6
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/status.html.haml6
-rw-r--r--app/views/issues/_issue.atom.builder6
-rw-r--r--app/views/koding/index.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml10
-rw-r--r--app/views/layouts/mailer.html.haml72
-rw-r--r--app/views/layouts/mailer.text.haml5
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml6
-rw-r--r--app/views/layouts/nav/_group.html.haml24
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml18
-rw-r--r--app/views/layouts/nav/_project.html.haml76
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml28
-rw-r--r--app/views/notify/build_fail_email.html.haml24
-rw-r--r--app/views/notify/build_fail_email.text.erb11
-rw-r--r--app/views/notify/build_success_email.html.haml24
-rw-r--r--app/views/notify/build_success_email.text.erb11
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml280
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml230
-rw-r--r--app/views/notify/pipeline_success_email.text.erb4
-rw-r--r--app/views/profiles/_head.html.haml1
-rw-r--r--app/views/profiles/accounts/show.html.haml7
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/profiles/personal_access_tokens/_form.html.haml21
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml75
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml3
-rw-r--r--app/views/profiles/update_username.js.haml7
-rw-r--r--app/views/projects/_activity.html.haml7
-rw-r--r--app/views/projects/_head.html.haml20
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml8
-rw-r--r--app/views/projects/activity.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml9
-rw-r--r--app/views/projects/blob/_actions.html.haml25
-rw-r--r--app/views/projects/blob/_blob.html.haml13
-rw-r--r--app/views/projects/blob/_header.html.haml39
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_text.html.haml14
-rw-r--r--app/views/projects/blob/diff.html.haml10
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml7
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml15
-rw-r--r--app/views/projects/boards/components/_board.html.haml2
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml27
-rw-r--r--app/views/projects/boards/components/_card.html.haml10
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/branches/new.html.haml8
-rw-r--r--app/views/projects/builds/_header.html.haml9
-rw-r--r--app/views/projects/builds/show.html.haml1
-rw-r--r--app/views/projects/buttons/_download.html.haml3
-rw-r--r--app/views/projects/buttons/_koding.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml92
-rw-r--r--app/views/projects/commit/_change.html.haml12
-rw-r--r--app/views/projects/commit/_commit_box.html.haml19
-rw-r--r--app/views/projects/commit/_pipelines_list.haml22
-rw-r--r--app/views/projects/commits/_commit.html.haml51
-rw-r--r--app/views/projects/commits/_commit_list.html.haml2
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml24
-rw-r--r--app/views/projects/commits/show.atom.builder2
-rw-r--r--app/views/projects/commits/show.html.haml10
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml3
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml34
-rw-r--r--app/views/projects/deploy_keys/index.html.haml36
-rw-r--r--app/views/projects/deployments/_actions.haml5
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml11
-rw-r--r--app/views/projects/diffs/_line.html.haml20
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml21
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml6
-rw-r--r--app/views/projects/environments/folder.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml6
-rw-r--r--app/views/projects/environments/metrics.html.haml24
-rw-r--r--app/views/projects/environments/show.html.haml3
-rw-r--r--app/views/projects/graphs/_head.html.haml19
-rw-r--r--app/views/projects/graphs/charts.html.haml127
-rw-r--r--app/views/projects/graphs/ci.html.haml18
-rw-r--r--app/views/projects/graphs/commits.html.haml95
-rw-r--r--app/views/projects/graphs/languages.html.haml33
-rw-r--r--app/views/projects/graphs/show.html.haml7
-rw-r--r--app/views/projects/issues/_form.html.haml6
-rw-r--r--app/views/projects/issues/_head.html.haml2
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/index.html.haml25
-rw-r--r--app/views/projects/issues/show.html.haml59
-rw-r--r--app/views/projects/issues/verify.html.haml3
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml13
-rw-r--r--app/views/projects/mattermosts/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/_form.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml5
-rw-r--r--app/views/projects/merge_requests/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml (renamed from app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml)0
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml1
-rw-r--r--app/views/projects/merge_requests/merge.js.haml4
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml60
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml16
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml26
-rw-r--r--app/views/projects/merge_requests/widget/open/_manual.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml28
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml33
-rw-r--r--app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml2
-rw-r--r--app/views/projects/milestones/edit.html.haml2
-rw-r--r--app/views/projects/milestones/index.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml7
-rw-r--r--app/views/projects/network/show.html.haml3
-rw-r--r--app/views/projects/new.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml29
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/pages/_use.html.haml4
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pipelines/_head.html.haml14
-rw-r--r--app/views/projects/pipelines/_stage.html.haml8
-rw-r--r--app/views/projects/pipelines/charts.html.haml21
-rw-r--r--app/views/projects/pipelines/charts/_build_times.haml (renamed from app/views/projects/graphs/ci/_build_times.haml)0
-rw-r--r--app/views/projects/pipelines/charts/_builds.haml (renamed from app/views/projects/graphs/ci/_builds.haml)0
-rw-r--r--app/views/projects/pipelines/charts/_overall.haml (renamed from app/views/projects/graphs/ci/_overall.haml)0
-rw-r--r--app/views/projects/pipelines/index.html.haml47
-rw-r--r--app/views/projects/pipelines/new.html.haml6
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/_index.html.haml21
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/index.html.haml22
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/settings/_head.html.haml33
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml1
-rw-r--r--app/views/projects/settings/integrations/show.html.haml1
-rw-r--r--app/views/projects/settings/members/show.html.haml1
-rw-r--r--app/views/projects/settings/repository/show.html.haml5
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml116
-rw-r--r--app/views/projects/snippets/show.html.haml8
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/destroy.js.haml4
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/projects/triggers/_content.html.haml14
-rw-r--r--app/views/projects/triggers/_form.html.haml11
-rw-r--r--app/views/projects/triggers/_index.html.haml24
-rw-r--r--app/views/projects/triggers/_trigger.html.haml40
-rw-r--r--app/views/projects/triggers/edit.html.haml9
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/shared/_branch_switcher.html.haml8
-rw-r--r--app/views/shared/_group_form.html.haml5
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml6
-rw-r--r--app/views/shared/_label.html.haml4
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/_milestones_filter.html.haml12
-rw-r--r--app/views/shared/_new_commit_form.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml39
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml60
-rw-r--r--app/views/shared/_sort_dropdown.html.haml3
-rw-r--r--app/views/shared/empty_states/_issues.html.haml5
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml18
-rw-r--r--app/views/shared/groups/_group.html.haml7
-rw-r--r--app/views/shared/groups/_search_form.html.haml2
-rw-r--r--app/views/shared/icons/_collapse.svg.erb1
-rw-r--r--app/views/shared/icons/_icon_customization.svg1
-rw-r--r--app/views/shared/icons/_icon_mattermost.svg1
-rw-r--r--app/views/shared/icons/_icon_mr_issue.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml20
-rw-r--r--app/views/shared/issuable/_form.html.haml56
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml98
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml37
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml6
-rw-r--r--app/views/shared/milestones/_issuables.html.haml2
-rw-r--r--app/views/shared/milestones/_summary.html.haml27
-rw-r--r--app/views/shared/milestones/_tabs.html.haml34
-rw-r--r--app/views/shared/projects/_dropdown.html.haml20
-rw-r--r--app/views/shared/projects/_list.html.haml5
-rw-r--r--app/views/shared/projects/_project.html.haml7
-rw-r--r--app/views/shared/projects/_search_form.html.haml23
-rw-r--r--app/views/shared/projects/blob/_branch_page_create.html.haml8
-rw-r--r--app/views/shared/projects/blob/_branch_page_default.html.haml10
-rw-r--r--app/views/shared/snippets/_blob.html.haml27
-rw-r--r--app/views/shared/snippets/_form.html.haml2
-rw-r--r--app/views/snippets/show.html.haml12
-rw-r--r--app/views/users/show.html.haml18
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/build_email_worker.rb20
-rw-r--r--app/workers/irker_worker.rb6
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--app/workers/stuck_ci_builds_worker.rb19
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb59
-rw-r--r--app/workers/system_hook_push_worker.rb8
-rw-r--r--app/workers/update_merge_requests_worker.rb3
-rw-r--r--app/workers/upload_checksum_worker.rb12
-rw-r--r--changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml4
-rw-r--r--changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml4
-rw-r--r--changelogs/unreleased/14748-runner-version-in-admin-views.yml4
-rw-r--r--changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml4
-rw-r--r--changelogs/unreleased/17662-rename-builds.yml4
-rw-r--r--changelogs/unreleased/18962-update-issues-button-jumps.yml4
-rw-r--r--changelogs/unreleased/1937-https-clone-url-username.yml4
-rw-r--r--changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml4
-rw-r--r--changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml4
-rw-r--r--changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml4
-rw-r--r--changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml4
-rw-r--r--changelogs/unreleased/21451-allow-disable-mr-link.yml4
-rw-r--r--changelogs/unreleased/21518_recaptcha_spam_issues.yml4
-rw-r--r--changelogs/unreleased/21605-allow-html5-details.yml4
-rw-r--r--changelogs/unreleased/22007-unify-projects-search.yml4
-rw-r--r--changelogs/unreleased/22466-task-list-alignment.yml4
-rw-r--r--changelogs/unreleased/22562-todos-filters.yml4
-rw-r--r--changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml4
-rw-r--r--changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml4
-rw-r--r--changelogs/unreleased/22974-trigger-service-events-through-api.yml4
-rw-r--r--changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml4
-rw-r--r--changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml4
-rw-r--r--changelogs/unreleased/23634-remove-project-grouping.yml4
-rw-r--r--changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml4
-rw-r--r--changelogs/unreleased/23948-assign-to-me.yml4
-rw-r--r--changelogs/unreleased/23993-drop-ci_projects.yml6
-rw-r--r--changelogs/unreleased/24137-issuable-permalink.yml4
-rw-r--r--changelogs/unreleased/24147-delete-env-button.yml4
-rw-r--r--changelogs/unreleased/24166-close-builds-dropdown.yml4
-rw-r--r--changelogs/unreleased/24421-personal-milestone-count-badges.yml4
-rw-r--r--changelogs/unreleased/24501-new-file-existing-branch.yml4
-rw-r--r--changelogs/unreleased/24606-force-password-reset-on-next-login.yml4
-rw-r--r--changelogs/unreleased/24683-sidebar-spinners.yml4
-rw-r--r--changelogs/unreleased/24716-fix-ctrl-click-links.yml4
-rw-r--r--changelogs/unreleased/24795_refactor_merge_request_build_service.yml4
-rw-r--r--changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml4
-rw-r--r--changelogs/unreleased/24923_nested_tasks.yml4
-rw-r--r--changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml4
-rw-r--r--changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml4
-rw-r--r--changelogs/unreleased/25312-search-input-cmd-click-issue.yml4
-rw-r--r--changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml4
-rw-r--r--changelogs/unreleased/25367-add-impersonation-token.yml4
-rw-r--r--changelogs/unreleased/25437-just-emoji.yml4
-rw-r--r--changelogs/unreleased/25460-replace-word-users-with-members.yml4
-rw-r--r--changelogs/unreleased/25503_issues_finder_performance.yml4
-rw-r--r--changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml4
-rw-r--r--changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml4
-rw-r--r--changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml4
-rw-r--r--changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml4
-rw-r--r--changelogs/unreleased/25920-create-issue-from-failing-build.yml4
-rw-r--r--changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml4
-rw-r--r--changelogs/unreleased/26059-segoe-ui-vertical.yml4
-rw-r--r--changelogs/unreleased/26068_tasklist_issue.yml4
-rw-r--r--changelogs/unreleased/26117-sort-pipeline-for-commit.yml4
-rw-r--r--changelogs/unreleased/26136-list-repository-tree-api-doc.yml4
-rw-r--r--changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml4
-rw-r--r--changelogs/unreleased/26188-tag-creation-404-for-guests.yml4
-rw-r--r--changelogs/unreleased/26202-change-dropdown-style-slightly.yml4
-rw-r--r--changelogs/unreleased/26236-monospace-gfm.yml4
-rw-r--r--changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml4
-rw-r--r--changelogs/unreleased/26348-cleanup-navigation-order-groups.yml4
-rw-r--r--changelogs/unreleased/26348-cleanup-navigation-order.yml4
-rw-r--r--changelogs/unreleased/26371-native-emojis-v3-code.yml4
-rw-r--r--changelogs/unreleased/26445-accessible-piplelines-buttons.yml4
-rw-r--r--changelogs/unreleased/26447-fix-tab-list-order.yml4
-rw-r--r--changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml4
-rw-r--r--changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml5
-rw-r--r--changelogs/unreleased/26787-add-copy-icon-hover-state.yml4
-rw-r--r--changelogs/unreleased/26790-label-color-todos.yml4
-rw-r--r--changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml5
-rw-r--r--changelogs/unreleased/26847-api-pipelines-use-basic.yml4
-rw-r--r--changelogs/unreleased/26852-fix-slug-for-openshift.yml4
-rw-r--r--changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml4
-rw-r--r--changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml4
-rw-r--r--changelogs/unreleased/26881-backup-fails-if-data-changes.yml4
-rw-r--r--changelogs/unreleased/26900-pipelines-tabs.yml4
-rw-r--r--changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml4
-rw-r--r--changelogs/unreleased/26947-build-status-self-link.yml4
-rw-r--r--changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml4
-rw-r--r--changelogs/unreleased/27013-regression-in-commit-title-bar.yml4
-rw-r--r--changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml4
-rw-r--r--changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml4
-rw-r--r--changelogs/unreleased/27032-add-a-house-keeping-api-call.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml4
-rw-r--r--changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml4
-rw-r--r--changelogs/unreleased/27174-filter-filters.yml4
-rw-r--r--changelogs/unreleased/27178-update-builds-link-in-project-settings.yml4
-rw-r--r--changelogs/unreleased/27240-make-progress-bars-consistent.yml4
-rw-r--r--changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml4
-rw-r--r--changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml4
-rw-r--r--changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml4
-rw-r--r--changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml4
-rw-r--r--changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml4
-rw-r--r--changelogs/unreleased/27352-search-label-filter-header.yml4
-rw-r--r--changelogs/unreleased/27354-navigation-new-button.yml4
-rw-r--r--changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml4
-rw-r--r--changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml4
-rw-r--r--changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml4
-rw-r--r--changelogs/unreleased/27484-environment-show-name.yml4
-rw-r--r--changelogs/unreleased/27488-fix-jwt-version.yml4
-rw-r--r--changelogs/unreleased/27494-environment-list-column-headers.yml4
-rw-r--r--changelogs/unreleased/27501-api-use-visibility-everywhere.yml4
-rw-r--r--changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml4
-rw-r--r--changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml4
-rw-r--r--changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml4
-rw-r--r--changelogs/unreleased/27532_api_changes.yml4
-rw-r--r--changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml4
-rw-r--r--changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml4
-rw-r--r--changelogs/unreleased/27632_fix_mr_widget_url.yml4
-rw-r--r--changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml4
-rw-r--r--changelogs/unreleased/27656-doc-ci-enable-ci.yml4
-rw-r--r--changelogs/unreleased/27762-add-default-artifacts-expiration.yml4
-rw-r--r--changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml4
-rw-r--r--changelogs/unreleased/27778-a11y-sidebar.yml5
-rw-r--r--changelogs/unreleased/27822-default-bulk-assign-labels.yml4
-rw-r--r--changelogs/unreleased/27840-improve-search-bar-experience.yml4
-rw-r--r--changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml4
-rw-r--r--changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml4
-rw-r--r--changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml4
-rw-r--r--changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml5
-rw-r--r--changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml4
-rw-r--r--changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml4
-rw-r--r--changelogs/unreleased/27934-left-align-logo.yml4
-rw-r--r--changelogs/unreleased/27934-left-align-nav.yml4
-rw-r--r--changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml4
-rw-r--r--changelogs/unreleased/27939-fix-current-build-arrow.yml4
-rw-r--r--changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml4
-rw-r--r--changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml4
-rw-r--r--changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml4
-rw-r--r--changelogs/unreleased/27963-tooltips-jobs.yml4
-rw-r--r--changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml4
-rw-r--r--changelogs/unreleased/27978-improve-task-list-ux.yml4
-rw-r--r--changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml4
-rw-r--r--changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml5
-rw-r--r--changelogs/unreleased/27991-success-with-warnings-caret.yml4
-rw-r--r--changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml4
-rw-r--r--changelogs/unreleased/28019-make-builds-show-faster.yml4
-rw-r--r--changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml4
-rw-r--r--changelogs/unreleased/28030-infinite-offset.yml4
-rw-r--r--changelogs/unreleased/28032-tooltips-file-name.yml5
-rw-r--r--changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml4
-rw-r--r--changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml4
-rw-r--r--changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml4
-rw-r--r--changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml4
-rw-r--r--changelogs/unreleased/28257-issues-iids.yml4
-rw-r--r--changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml4
-rw-r--r--changelogs/unreleased/28357-colon-search.yml4
-rw-r--r--changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml4
-rw-r--r--changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml4
-rw-r--r--changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml4
-rw-r--r--changelogs/unreleased/28410-dropdown-styling.yml4
-rw-r--r--changelogs/unreleased/28447-hybrid-repository-storages.yml4
-rw-r--r--changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml4
-rw-r--r--changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml4
-rw-r--r--changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml4
-rw-r--r--changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml4
-rw-r--r--changelogs/unreleased/28516-default-kubernetes-namespace.yml4
-rw-r--r--changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml4
-rw-r--r--changelogs/unreleased/28538-restore-nav-shortcuts.yml4
-rw-r--r--changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml4
-rw-r--r--changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml4
-rw-r--r--changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml4
-rw-r--r--changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml4
-rw-r--r--changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml4
-rw-r--r--changelogs/unreleased/28723-consistent-handling-indexof.yml4
-rw-r--r--changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml4
-rw-r--r--changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml4
-rw-r--r--changelogs/unreleased/28835-jobs-head.yml4
-rw-r--r--changelogs/unreleased/28837-remove-help-duplicate.yml4
-rw-r--r--changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml4
-rw-r--r--changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml4
-rw-r--r--changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml5
-rw-r--r--changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml4
-rw-r--r--changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml4
-rw-r--r--changelogs/unreleased/28935-make-logo-smaller.yml4
-rw-r--r--changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml4
-rw-r--r--changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml4
-rw-r--r--changelogs/unreleased/29034-fix-github-importer.yml4
-rw-r--r--changelogs/unreleased/29046-fix-github-importer-open-prs.yml4
-rw-r--r--changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml4
-rw-r--r--changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml4
-rw-r--r--changelogs/unreleased/29189-discussion-button.yml4
-rw-r--r--changelogs/unreleased/29209-sign-up-form-name.yml4
-rw-r--r--changelogs/unreleased/29263-merge-button-color.yml4
-rw-r--r--changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml4
-rw-r--r--changelogs/unreleased/29405-fix-project-wiki-update.yml4
-rw-r--r--changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml4
-rw-r--r--changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml4
-rw-r--r--changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml4
-rw-r--r--changelogs/unreleased/29604-v3-fix-branch-creation.yml4
-rw-r--r--changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml4
-rw-r--r--changelogs/unreleased/3440-remove-hsts-header.yml4
-rw-r--r--changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml4
-rw-r--r--changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml4
-rw-r--r--changelogs/unreleased/6073_project_api.yml4
-rw-r--r--changelogs/unreleased/8-15-stable.yml4
-rw-r--r--changelogs/unreleased/8082-permalink-to-file.yml4
-rw-r--r--changelogs/unreleased/9-0-api-changes.yml4
-rw-r--r--changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml4
-rw-r--r--changelogs/unreleased/adam-prevent-two-issue-trackers.yml4
-rw-r--r--changelogs/unreleased/add-blob-copy-button.yml4
-rw-r--r--changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml4
-rw-r--r--changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml4
-rw-r--r--changelogs/unreleased/add-git-version-to-system-info.yml4
-rw-r--r--changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml4
-rw-r--r--changelogs/unreleased/add-labels-to-issue-hook.yml4
-rw-r--r--changelogs/unreleased/add-pipeline-triggers.yml4
-rw-r--r--changelogs/unreleased/add_mr_info_to_issues_list.yml4
-rw-r--r--changelogs/unreleased/add_project_update_hook.yml4
-rw-r--r--changelogs/unreleased/add_quick_submit_for_snippets_form.yml4
-rw-r--r--changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml4
-rw-r--r--changelogs/unreleased/api-drop-subscribed.yml5
-rw-r--r--changelogs/unreleased/api-empty-return.yml4
-rw-r--r--changelogs/unreleased/api-notes-entity-fields.yml4
-rw-r--r--changelogs/unreleased/api-project-issues-404.yml4
-rw-r--r--changelogs/unreleased/api-remove-owned-groups.yml4
-rw-r--r--changelogs/unreleased/api-remove-snippets-expires-at.yml4
-rw-r--r--changelogs/unreleased/babel-all-the-things.yml5
-rw-r--r--changelogs/unreleased/backup_storage_class.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting-2.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting.yml4
-rw-r--r--changelogs/unreleased/branch_deletion.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/bugfix-systemhook.yml4
-rw-r--r--changelogs/unreleased/change_queue_weight.yml4
-rw-r--r--changelogs/unreleased/chore-23493-remaining-time-tooltip.yml5
-rw-r--r--changelogs/unreleased/clear-connections-before-starting-sidekiq.yml4
-rw-r--r--changelogs/unreleased/clipboard-button-commit-sha.yml3
-rw-r--r--changelogs/unreleased/commit-search-ui-fix.yml4
-rw-r--r--changelogs/unreleased/commons-chunk-plugin.yml5
-rw-r--r--changelogs/unreleased/contribution-calendar-scroll.yml4
-rw-r--r--changelogs/unreleased/cop-gem-fetcher.yml4
-rw-r--r--changelogs/unreleased/copy-as-md.yml4
-rw-r--r--changelogs/unreleased/create_branch_repo_less.yml4
-rw-r--r--changelogs/unreleased/dashboard-filter-search-keep-params.yml4
-rw-r--r--changelogs/unreleased/delete-artifacts-for-pages.yml4
-rw-r--r--changelogs/unreleased/diff-make-obvious-cant-comment.yml4
-rw-r--r--changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml4
-rw-r--r--changelogs/unreleased/display-project-id.yml4
-rw-r--r--changelogs/unreleased/dm-copy-code-as-gfm.yml4
-rw-r--r--changelogs/unreleased/dm-group-reference-full-name.yml4
-rw-r--r--changelogs/unreleased/document-how-to-vue.yml4
-rw-r--r--changelogs/unreleased/dynamic-todos-fixture.yml4
-rw-r--r--changelogs/unreleased/dz-blacklist--names.yml4
-rw-r--r--changelogs/unreleased/dz-change-project-view.yml4
-rw-r--r--changelogs/unreleased/dz-dashboard-groups-search.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-improvements-2.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-members.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-restrictions.yml4
-rw-r--r--changelogs/unreleased/empty-selection-reply-shortcut.yml4
-rw-r--r--changelogs/unreleased/enable-snippets-by-default.yml4
-rw-r--r--changelogs/unreleased/es6-class-issue.yml4
-rw-r--r--changelogs/unreleased/etag-notes-polling.yml4
-rw-r--r--changelogs/unreleased/expose-pagination-headers.yml4
-rw-r--r--changelogs/unreleased/fe-commit-mr-pipelines.yml4
-rw-r--r--changelogs/unreleased/feature-brand-logo-in-emails.yml4
-rw-r--r--changelogs/unreleased/feature-custom-lfs.yml4
-rw-r--r--changelogs/unreleased/feature-openid-connect.yml4
-rw-r--r--changelogs/unreleased/feature-runner-jobs-v4-api.yml4
-rw-r--r--changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml4
-rw-r--r--changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml4
-rw-r--r--changelogs/unreleased/feature-syshook_commits.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-show.yml4
-rw-r--r--changelogs/unreleased/fix-27479.yml4
-rw-r--r--changelogs/unreleased/fix-29093.yml4
-rw-r--r--changelogs/unreleased/fix-api-mr-permissions.yml4
-rw-r--r--changelogs/unreleased/fix-ar-connection-leaks.yml4
-rw-r--r--changelogs/unreleased/fix-ci-build-policy.yml4
-rw-r--r--changelogs/unreleased/fix-deleting-project-again.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml4
-rw-r--r--changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml4
-rw-r--r--changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml4
-rw-r--r--changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml4
-rw-r--r--changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml4
-rw-r--r--changelogs/unreleased/fix-gb-update-commit-status-api.yml4
-rw-r--r--changelogs/unreleased/fix-guest-access-posting-to-notes.yml4
-rw-r--r--changelogs/unreleased/fix-import-encrypt-atts.yml4
-rw-r--r--changelogs/unreleased/fix-import-group-members.yml4
-rw-r--r--changelogs/unreleased/fix-job-to-pipeline-renaming.yml4
-rw-r--r--changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-name-on-show.yml4
-rw-r--r--changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml4
-rw-r--r--changelogs/unreleased/fix-references-header-parsing.yml5
-rw-r--r--changelogs/unreleased/fix-scroll-test.yml4
-rw-r--r--changelogs/unreleased/fix-slow-queries-for-branches-index.yml4
-rw-r--r--changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml4
-rw-r--r--changelogs/unreleased/fix_broken_diff_discussions.yml4
-rw-r--r--changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml4
-rw-r--r--changelogs/unreleased/fix_updated_field_in_issues-atom.yml4
-rw-r--r--changelogs/unreleased/fix_visibility_level.yml4
-rw-r--r--changelogs/unreleased/fl-remove-ujs-pipelines.yml4
-rw-r--r--changelogs/unreleased/format-timeago-date.yml4
-rw-r--r--changelogs/unreleased/fwn-to-find-by-full-path.yml4
-rw-r--r--changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml4
-rw-r--r--changelogs/unreleased/git_to_html_redirection.yml4
-rw-r--r--changelogs/unreleased/gitaly-post-receive.yml4
-rw-r--r--changelogs/unreleased/go-go-gadget-webpack.yml4
-rw-r--r--changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/handle-failure-when-deleting-tags.yml4
-rw-r--r--changelogs/unreleased/hardcode-title-system-note.yml4
-rw-r--r--changelogs/unreleased/improve-ci-example-php-doc.yml4
-rw-r--r--changelogs/unreleased/improve-handleLocationHash-tests.yml4
-rw-r--r--changelogs/unreleased/introduce-pipeline-triggers.yml4
-rw-r--r--changelogs/unreleased/issuable-sidebar-bug.yml4
-rw-r--r--changelogs/unreleased/issue-20428.yml4
-rw-r--r--changelogs/unreleased/issue-boards-new-search-bar.yml4
-rw-r--r--changelogs/unreleased/issue-descrpiption-spinner-off.yml4
-rw-r--r--changelogs/unreleased/issue-sidebar-empty-assignee.yml4
-rw-r--r--changelogs/unreleased/issue-tags-layout.yml4
-rw-r--r--changelogs/unreleased/issue_16834.yml4
-rw-r--r--changelogs/unreleased/issue_19262.yml4
-rw-r--r--changelogs/unreleased/issue_23317.yml4
-rw-r--r--changelogs/unreleased/issue_24815.yml4
-rw-r--r--changelogs/unreleased/issue_25112.yml4
-rw-r--r--changelogs/unreleased/issue_27211.yml4
-rw-r--r--changelogs/unreleased/issue_27212.yml4
-rw-r--r--changelogs/unreleased/issue_28051_2.yml4
-rw-r--r--changelogs/unreleased/issue_29449.yml4
-rw-r--r--changelogs/unreleased/jej-pages-picked-from-ee.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/lfs-noauth-public-repo.yml4
-rw-r--r--changelogs/unreleased/list_issues_with_no_labels.yml4
-rw-r--r--changelogs/unreleased/long-file-name-overflow.yml4
-rw-r--r--changelogs/unreleased/make-karma-fast-again.yml4
-rw-r--r--changelogs/unreleased/markdown-plantuml.yml4
-rw-r--r--changelogs/unreleased/merge-request-tabs-fixture.yml4
-rw-r--r--changelogs/unreleased/migrate-pipeline-events-and-email-service.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/mock-ci-service.yml4
-rw-r--r--changelogs/unreleased/moving-issue-with-two-list-labels.yml4
-rw-r--r--changelogs/unreleased/mr-diff-comment-button.yml4
-rw-r--r--changelogs/unreleased/mr-tabs-container-offset.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.yml4
-rw-r--r--changelogs/unreleased/no-sidebar-on-action-btn-click.yml4
-rw-r--r--changelogs/unreleased/no_project_notes.yml4
-rw-r--r--changelogs/unreleased/only-create-unmergeable-todo-once.yml4
-rw-r--r--changelogs/unreleased/option-to-be-notified-of-own-activity.yml4
-rw-r--r--changelogs/unreleased/pages-0-4-0.yml4
-rw-r--r--changelogs/unreleased/pipeline-blocking-actions.yml4
-rw-r--r--changelogs/unreleased/pipeline-tooltips-overflow.yml4
-rw-r--r--changelogs/unreleased/pms-lowercase-system-notes.yml4
-rw-r--r--changelogs/unreleased/priority-to-label-priority.yml4
-rw-r--r--changelogs/unreleased/redesign-searchbar-admin-project-26794.yml4
-rw-r--r--changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml5
-rw-r--r--changelogs/unreleased/refresh-permissions-recent-users.yml4
-rw-r--r--changelogs/unreleased/relative-url-assets.yml4
-rw-r--r--changelogs/unreleased/remove-deploy-key-endpoint.yml4
-rw-r--r--changelogs/unreleased/remove-es6-extension.yml4
-rw-r--r--changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-plugins.yml4
-rw-r--r--changelogs/unreleased/remove-new-relic-gem.yml4
-rw-r--r--changelogs/unreleased/remove-readme-option.yml4
-rw-r--r--changelogs/unreleased/remove-subscribe-label-tooltip.yml4
-rw-r--r--changelogs/unreleased/remove-unused-ci-tables.yml4
-rw-r--r--changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml4
-rw-r--r--changelogs/unreleased/rename_all_issues.yml4
-rw-r--r--changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml4
-rw-r--r--changelogs/unreleased/route-map.yml4
-rw-r--r--changelogs/unreleased/routes-lower-case.yml4
-rw-r--r--changelogs/unreleased/rs-warden-blocked-users.yml4
-rw-r--r--changelogs/unreleased/rss-btn-alignment-fix.yml4
-rw-r--r--changelogs/unreleased/set-default-cache-key-for-jobs.yml4
-rw-r--r--changelogs/unreleased/settings-tab.yml4
-rw-r--r--changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml4
-rw-r--r--changelogs/unreleased/sh-add-labels-index.yml4
-rw-r--r--changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml4
-rw-r--r--changelogs/unreleased/simplify-docs-trigger.yml4
-rw-r--r--changelogs/unreleased/slash-commands-typo.yml4
-rw-r--r--changelogs/unreleased/small-screen-fullscreen-button.yml4
-rw-r--r--changelogs/unreleased/snippets-search-performance.yml4
-rw-r--r--changelogs/unreleased/sort-builds-in-stage-dropdown.yml4
-rw-r--r--changelogs/unreleased/ssh-key-paste.yml4
-rw-r--r--changelogs/unreleased/ssrf-protections.yml4
-rw-r--r--changelogs/unreleased/tc-api-pipeline-jobs.yml4
-rw-r--r--changelogs/unreleased/tc-fix-project-create-500.yml4
-rw-r--r--changelogs/unreleased/tc-only-mr-button-if-allowed.yml4
-rw-r--r--changelogs/unreleased/terminal-max-session-time.yml4
-rw-r--r--changelogs/unreleased/unified-member-api-response.yml4
-rw-r--r--changelogs/unreleased/update-ace.yml4
-rw-r--r--changelogs/unreleased/update-vue-2-1.yml4
-rw-r--r--changelogs/unreleased/updated-pages-0-3-1.yml4
-rw-r--r--changelogs/unreleased/upgrade-babel-v6.yml4
-rw-r--r--changelogs/unreleased/upgrade-omniauth.yml4
-rw-r--r--changelogs/unreleased/upgrade-webpack-v2-2.yml4
-rw-r--r--changelogs/unreleased/use-corejs-polyfills.yml4
-rw-r--r--changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml4
-rw-r--r--changelogs/unreleased/user-calendar-border.yml4
-rw-r--r--changelogs/unreleased/user-callouts.yml4
-rw-r--r--changelogs/unreleased/wip-mr-from-commits.yml4
-rw-r--r--changelogs/unreleased/workhorse-1-4-0.yml4
-rw-r--r--changelogs/unreleased/zj-builds-to-jobs-api.yml4
-rw-r--r--changelogs/unreleased/zj-format-chat-messages.yml4
-rw-r--r--changelogs/unreleased/zj-remove-deprecated-ci-service.yml4
-rw-r--r--changelogs/unreleased/zj-requeue-pending-delete.yml4
-rw-r--r--changelogs/unreleased/zj-variables-build-job.yml4
-rw-r--r--config/application.rb12
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/gitlab.yml.example42
-rw-r--r--config/initializers/0_inflections.rb (renamed from config/initializers/inflections.rb)0
-rw-r--r--config/initializers/1_settings.rb66
-rw-r--r--config/initializers/6_validations.rb28
-rw-r--r--config/initializers/8_gitaly.rb2
-rw-r--r--config/initializers/8_metrics.rb192
-rw-r--r--config/initializers/acts_as_taggable.rb5
-rw-r--r--config/initializers/devise.rb8
-rw-r--r--config/initializers/doorkeeper.rb11
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb36
-rw-r--r--config/initializers/etag_caching.rb4
-rw-r--r--config/initializers/fix_local_cache_middleware.rb24
-rw-r--r--config/initializers/gollum.rb2
-rw-r--r--config/initializers/health_check.rb4
-rw-r--r--config/initializers/metrics.rb188
-rw-r--r--config/initializers/omniauth.rb9
-rw-r--r--config/initializers/request_context.rb3
-rw-r--r--config/initializers/rspec_profiling.rb37
-rw-r--r--config/initializers/secret_token.rb7
-rw-r--r--config/initializers/sidekiq.rb8
-rw-r--r--config/initializers/trusted_proxies.rb2
-rw-r--r--config/initializers/warden.rb5
-rw-r--r--config/initializers/workhorse_multipart.rb2
-rw-r--r--config/karma.config.js39
-rw-r--r--config/locales/doorkeeper.en.yml1
-rw-r--r--config/newrelic.yml16
-rw-r--r--config/routes.rb5
-rw-r--r--config/routes/admin.rb5
-rw-r--r--config/routes/ci.rb8
-rw-r--r--config/routes/dashboard.rb1
-rw-r--r--config/routes/profile.rb2
-rw-r--r--config/routes/project.rb17
-rw-r--r--config/routes/wiki.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js93
-rw-r--r--db/fixtures/development/13_comments.rb4
-rw-r--r--db/fixtures/development/15_award_emoji.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb10
-rw-r--r--db/fixtures/development/19_nested_groups.rb69
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb2
-rw-r--r--db/migrate/20160610201627_migrate_users_notification_level.rb4
-rw-r--r--db/migrate/20160615142710_add_index_on_requested_at_to_members.rb8
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb8
-rw-r--r--db/migrate/20160715134306_add_index_for_pipeline_user_id.rb8
-rw-r--r--db/migrate/20160805041956_add_deleted_at_to_namespaces.rb9
-rw-r--r--db/migrate/20160808085602_add_index_for_build_token.rb6
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb6
-rw-r--r--db/migrate/20160819232256_add_incoming_email_token_to_users.rb9
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb2
-rw-r--r--db/migrate/20160831214543_migrate_project_features.rb2
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb8
-rw-r--r--db/migrate/20160920160832_add_index_to_labels_title.rb6
-rw-r--r--db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb4
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb10
-rw-r--r--db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb4
-rw-r--r--db/migrate/20161031171301_add_project_id_to_subscriptions.rb1
-rw-r--r--db/migrate/20161106185620_add_project_import_data_project_index.rb6
-rw-r--r--db/migrate/20161124111395_add_index_to_parent_id.rb6
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb6
-rw-r--r--db/migrate/20161128142110_remove_unnecessary_indexes.rb4
-rw-r--r--db/migrate/20161201160452_migrate_project_statistics.rb5
-rw-r--r--db/migrate/20161202152035_add_index_to_routes.rb7
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb8
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb4
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb6
-rw-r--r--db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb37
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb2
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb2
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb4
-rw-r--r--db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb18
-rw-r--r--db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb18
-rw-r--r--db/migrate/20170120131253_create_chat_teams.rb18
-rw-r--r--db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb14
-rw-r--r--db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb3
-rw-r--r--db/migrate/20170130204620_add_index_to_project_authorizations.rb5
-rw-r--r--db/migrate/20170130221926_create_uploads.rb20
-rw-r--r--db/migrate/20170131221752_add_relative_position_to_issues.rb37
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb6
-rw-r--r--db/migrate/20170206115204_add_column_ghost_to_users.rb11
-rw-r--r--db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb7
-rw-r--r--db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb6
-rw-r--r--db/migrate/20170210103609_add_index_to_user_agent_detail.rb8
-rw-r--r--db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb17
-rw-r--r--db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb11
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb14
-rw-r--r--db/migrate/20170216141440_drop_index_for_builds_project_status.rb8
-rw-r--r--db/migrate/20170217132157_rename_merge_when_build_succeeds.rb29
-rw-r--r--db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb29
-rw-r--r--db/migrate/20170217151948_add_owner_id_to_triggers.rb9
-rw-r--r--db/migrate/20170217151949_add_description_to_triggers.rb9
-rw-r--r--db/migrate/20170222143317_drop_ci_projects.rb34
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb28
-rw-r--r--db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb14
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb18
-rw-r--r--db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb10
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb83
-rw-r--r--db/migrate/20170305203726_add_owner_id_foreign_key.rb15
-rw-r--r--db/migrate/20170313213916_add_index_to_user_ghost.rb24
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb24
-rw-r--r--db/migrate/20170315194013_add_closed_at_to_issues.rb7
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb14
-rw-r--r--db/post_migrate/20161221153951_rename_reserved_project_names.rb2
-rw-r--r--db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb19
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb3
-rw-r--r--db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb20
-rw-r--r--db/post_migrate/20170211073944_disable_invalid_service_templates.rb6
-rw-r--r--db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb87
-rw-r--r--db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb23
-rw-r--r--db/post_migrate/20170309171644_reset_relative_position_for_issue.rb17
-rw-r--r--db/post_migrate/20170313133418_rename_more_reserved_project_names.rb72
-rw-r--r--db/schema.rb167
-rw-r--r--doc/README.md3
-rw-r--r--doc/administration/auth/crowd.md68
-rw-r--r--doc/administration/auth/img/crowd_application.pngbin0 -> 55811 bytes
-rw-r--r--doc/administration/container_registry.md98
-rw-r--r--doc/administration/high_availability/database.md4
-rw-r--r--doc/administration/high_availability/load_balancer.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md2
-rw-r--r--doc/administration/monitoring/prometheus/index.md53
-rw-r--r--doc/administration/pages/index.md47
-rw-r--r--doc/administration/pages/source.md102
-rw-r--r--doc/administration/reply_by_email.md60
-rw-r--r--doc/administration/repository_storage_paths.md15
-rw-r--r--doc/api/README.md29
-rw-r--r--doc/api/access_requests.md16
-rw-r--r--doc/api/award_emoji.md166
-rw-r--r--doc/api/boards.md25
-rw-r--r--doc/api/branches.md27
-rw-r--r--doc/api/broadcast_messages.md24
-rw-r--r--doc/api/build_triggers.md119
-rw-r--r--doc/api/build_variables.md17
-rw-r--r--doc/api/builds.md611
-rw-r--r--doc/api/ci/builds.md2
-rw-r--r--doc/api/ci/lint.md2
-rw-r--r--doc/api/commits.md18
-rw-r--r--doc/api/deploy_key_multiple_projects.md8
-rw-r--r--doc/api/deploy_keys.md24
-rw-r--r--doc/api/deployments.md4
-rw-r--r--doc/api/enviroments.md31
-rw-r--r--doc/api/groups.md63
-rw-r--r--doc/api/issues.md254
-rw-r--r--doc/api/jobs.md622
-rw-r--r--doc/api/labels.md46
-rw-r--r--doc/api/members.md20
-rw-r--r--doc/api/merge_requests.md245
-rw-r--r--doc/api/milestones.md14
-rw-r--r--doc/api/namespaces.md4
-rw-r--r--doc/api/notes.md90
-rw-r--r--doc/api/notification_settings.md12
-rw-r--r--doc/api/oauth2.md4
-rw-r--r--doc/api/pipeline_triggers.md170
-rw-r--r--doc/api/pipelines.md54
-rw-r--r--doc/api/project_snippets.md16
-rw-r--r--doc/api/projects.md136
-rw-r--r--doc/api/repositories.md14
-rw-r--r--doc/api/repository_files.md39
-rw-r--r--doc/api/runners.md40
-rw-r--r--doc/api/services.md75
-rw-r--r--doc/api/session.md2
-rw-r--r--doc/api/settings.md27
-rw-r--r--doc/api/sidekiq_metrics.md8
-rw-r--r--doc/api/snippets.md29
-rw-r--r--doc/api/system_hooks.md27
-rw-r--r--doc/api/tags.md11
-rw-r--r--doc/api/templates/gitignores.md4
-rw-r--r--doc/api/templates/gitlab_ci_ymls.md4
-rw-r--r--doc/api/templates/licenses.md4
-rw-r--r--doc/api/todos.md12
-rw-r--r--doc/api/users.md100
-rw-r--r--doc/api/v3_to_v4.md97
-rw-r--r--doc/api/version.md2
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md14
-rw-r--r--doc/ci/docker/using_docker_images.md10
-rw-r--r--doc/ci/environments.md38
-rw-r--r--doc/ci/quick_start/README.md15
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/ci/ssh_keys/README.md18
-rw-r--r--doc/ci/triggers/README.md16
-rw-r--r--doc/ci/variables/README.md244
-rw-r--r--doc/ci/yaml/README.md74
-rw-r--r--doc/customization/branded_page_and_email_header.md15
-rw-r--r--doc/customization/branded_page_and_email_header/appearance.pngbin0 -> 10253 bytes
-rw-r--r--doc/customization/branded_page_and_email_header/custom_brand_header.pngbin0 -> 10014 bytes
-rw-r--r--doc/customization/branded_page_and_email_header/custom_email_header.pngbin0 -> 37472 bytes
-rw-r--r--doc/development/changelog.md109
-rw-r--r--doc/development/ci_setup.md3
-rw-r--r--doc/development/doc_styleguide.md16
-rw-r--r--doc/development/frontend.md79
-rw-r--r--doc/development/instrumentation.md2
-rw-r--r--doc/development/limit_ee_conflicts.md9
-rw-r--r--doc/development/merge_request_performance_guidelines.md4
-rw-r--r--doc/development/performance.md1
-rw-r--r--doc/development/polling.md41
-rw-r--r--doc/development/profiling.md2
-rw-r--r--doc/development/query_recorder.md29
-rw-r--r--doc/development/testing.md4
-rw-r--r--doc/development/ux_guide/copy.md9
-rw-r--r--doc/development/ux_guide/img/karolina-plaskaty.pngbin0 -> 33498 bytes
-rw-r--r--doc/development/ux_guide/img/nazim-ramesh.pngbin0 -> 31163 bytes
-rw-r--r--doc/development/ux_guide/users.md38
-rw-r--r--doc/downgrade_ee_to_ce/README.md7
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin4196 -> 6978 bytes
-rw-r--r--doc/install/README.md37
-rw-r--r--doc/install/google-protobuf.md26
-rw-r--r--doc/install/google_cloud_platform/img/change_admin_passwd_email.pngbin0 -> 7193 bytes
-rw-r--r--doc/install/google_cloud_platform/img/chrome_not_secure_page.pngbin0 -> 21705 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.pngbin0 -> 23486 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gcp_gitlab_overview.pngbin0 -> 42028 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gcp_landing.pngbin0 -> 59912 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.pngbin0 -> 42090 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gcp_search_for_gitlab.pngbin0 -> 7648 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gitlab_deployed_page.pngbin0 -> 35573 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gitlab_first_sign_in.pngbin0 -> 20054 bytes
-rw-r--r--doc/install/google_cloud_platform/img/gitlab_launch_button.pngbin0 -> 5198 bytes
-rw-r--r--doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.pngbin0 -> 50014 bytes
-rw-r--r--doc/install/google_cloud_platform/img/ssh_via_button.pngbin0 -> 3062 bytes
-rw-r--r--doc/install/google_cloud_platform/index.md168
-rw-r--r--doc/install/installation.md17
-rw-r--r--doc/install/requirements.md14
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/auth0.md2
-rw-r--r--doc/integration/crowd.md64
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/integration/openid_connect_provider.md47
-rw-r--r--doc/integration/saml.md10
-rw-r--r--doc/pages/README.md2
-rw-r--r--doc/pages/getting_started_part_one.md1
-rw-r--r--doc/pages/getting_started_part_three.md1
-rw-r--r--doc/pages/getting_started_part_two.md1
-rw-r--r--doc/project_services/builds_emails.md1
-rw-r--r--doc/raketasks/backup_restore.md36
-rw-r--r--doc/ssh/README.md99
-rw-r--r--doc/system_hooks/system_hooks.md15
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/university/support/README.md1
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/update/8.16-to-8.17.md14
-rw-r--r--doc/update/8.17-to-9.0.md330
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md24
-rw-r--r--doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.pngbin0 -> 14656 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.pngbin3447 -> 12917 bytes
-rw-r--r--doc/user/award_emojis.md51
-rw-r--r--doc/user/group/subgroups/img/create_new_group.pngbin0 -> 18503 bytes
-rw-r--r--doc/user/group/subgroups/img/create_subgroup_button.pngbin0 -> 8402 bytes
-rw-r--r--doc/user/group/subgroups/img/group_members.pngbin0 -> 48240 bytes
-rw-r--r--doc/user/group/subgroups/img/mention_subgroups.pngbin0 -> 39666 bytes
-rw-r--r--doc/user/group/subgroups/index.md164
-rw-r--r--doc/user/img/award_emoji_comment_awarded.png (renamed from doc/workflow/img/award_emoji_comment_awarded.png)bin19159 -> 19159 bytes
-rw-r--r--doc/user/img/award_emoji_comment_picker.png (renamed from doc/workflow/img/award_emoji_comment_picker.png)bin72883 -> 72883 bytes
-rw-r--r--doc/user/img/award_emoji_select.pngbin0 -> 17827 bytes
-rw-r--r--doc/user/img/award_emoji_votes_sort_options.pngbin0 -> 99941 bytes
-rw-r--r--doc/user/markdown.md4
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/profile/account/two_factor_authentication.md13
-rw-r--r--doc/user/project/container_registry.md2
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.pngbin73502 -> 249592 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.pngbin0 -> 113092 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_gcp_firewall_rule.pngbin0 -> 15247 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_gcp_node_name.pngbin0 -> 52622 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_service_configuration.pngbin0 -> 18100 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_yaml_deploy.pngbin0 -> 23567 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.pngbin29825 -> 229050 bytes
-rw-r--r--doc/user/project/integrations/kubernetes.md3
-rw-r--r--doc/user/project/integrations/mattermost.md13
-rw-r--r--doc/user/project/integrations/mock_ci.md13
-rw-r--r--doc/user/project/integrations/project_services.md3
-rw-r--r--doc/user/project/integrations/prometheus.md196
-rw-r--r--doc/user/project/integrations/samples/prometheus.yml69
-rw-r--r--doc/user/project/integrations/slack.md12
-rw-r--r--doc/user/project/integrations/webhooks.md14
-rw-r--r--doc/user/project/issue_board.md11
-rw-r--r--doc/user/project/labels.md13
-rw-r--r--doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.pngbin0 -> 29007 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.pngbin39796 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.pngbin12063 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_status.pngbin48458 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.pngbin0 -> 60346 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png (renamed from doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png)bin5251 -> 5251 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.pngbin0 -> 25783 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.pngbin0 -> 69953 bytes
-rw-r--r--doc/user/project/merge_requests/img/new_issue_for_discussion.pngbin0 -> 39563 bytes
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussion.pngbin0 -> 82412 bytes
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussions.pngbin178361 -> 143871 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_issue_notice.pngbin11123 -> 10307 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md33
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md11
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md4
-rw-r--r--doc/user/project/pages/getting_started_part_four.md385
-rw-r--r--doc/user/project/pages/getting_started_part_one.md106
-rw-r--r--doc/user/project/pages/getting_started_part_three.md190
-rw-r--r--doc/user/project/pages/getting_started_part_two.md154
-rw-r--r--doc/user/project/pages/img/add_certificate_to_pages.pngbin0 -> 14608 bytes
-rw-r--r--doc/user/project/pages/img/choose_ci_template.pngbin0 -> 23532 bytes
-rw-r--r--doc/user/project/pages/img/dns_add_new_a_record_example_updated.pngbin0 -> 10578 bytes
-rw-r--r--doc/user/project/pages/img/dns_cname_record_example.pngbin0 -> 4983 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin33597 -> 6063 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin87071 -> 14435 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin34686 -> 5351 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin63716 -> 12936 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin51136 -> 8763 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin27259 -> 3810 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin103730 -> 22907 bytes
-rw-r--r--doc/user/project/pages/img/remove_fork_relashionship.pngbin0 -> 13642 bytes
-rw-r--r--doc/user/project/pages/img/setup_ci.pngbin0 -> 10032 bytes
-rw-r--r--doc/user/project/pages/index.md486
-rw-r--r--doc/user/project/pages/introduction.md447
-rw-r--r--doc/user/project/repository/web_editor.md7
-rw-r--r--doc/user/project/settings/import_export.md2
-rw-r--r--doc/user/project/slash_commands.md1
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/award_emoji.md66
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin23779 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin50191 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin48342 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin57145 -> 0 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md13
-rw-r--r--doc/workflow/milestones.md19
-rw-r--r--doc/workflow/shortcuts.md2
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/project/active_tab.feature49
-rw-r--r--features/project/commits/branches.feature8
-rw-r--r--features/project/graph.feature12
-rw-r--r--features/project/issues/award_emoji.feature2
-rw-r--r--features/project/shortcuts.feature15
-rw-r--r--features/steps/dashboard/todos.rb6
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/group/milestones.rb4
-rw-r--r--features/steps/project/active_tab.rb48
-rw-r--r--features/steps/project/builds/artifacts.rb2
-rw-r--r--features/steps/project/commits/branches.rb24
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/commits/revert.rb1
-rw-r--r--features/steps/project/deploy_keys.rb2
-rw-r--r--features/steps/project/fork.rb2
-rw-r--r--features/steps/project/graph.rb4
-rw-r--r--features/steps/project/issues/award_emoji.rb6
-rw-r--r--features/steps/project/network_graph.rb2
-rw-r--r--features/steps/project/pages.rb6
-rw-r--r--features/steps/project/source/browse_files.rb10
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--features/steps/shared/paths.rb2
-rw-r--r--features/steps/shared/project.rb8
-rw-r--r--features/steps/shared/project_tab.rb32
-rw-r--r--features/support/capybara.rb11
-rw-r--r--fixtures/emojis/digests.json15206
-rw-r--r--fixtures/emojis/emoji-unicode-version-map.json2377
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/api.rb25
-rw-r--r--lib/api/api_guard.rb17
-rw-r--r--lib/api/award_emoji.rb32
-rw-r--r--lib/api/boards.rb6
-rw-r--r--lib/api/branches.rb12
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/builds.rb261
-rw-r--r--lib/api/commit_statuses.rb18
-rw-r--r--lib/api/commits.rb41
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deployments.rb4
-rw-r--r--lib/api/entities.rb179
-rw-r--r--lib/api/environments.rb21
-rw-r--r--lib/api/files.rb69
-rw-r--r--lib/api/groups.rb26
-rw-r--r--lib/api/helpers.rb55
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/helpers/runner.rb69
-rw-r--r--lib/api/internal.rb12
-rw-r--r--lib/api/issues.rb53
-rw-r--r--lib/api/jobs.rb252
-rw-r--r--lib/api/labels.rb6
-rw-r--r--lib/api/members.rb36
-rw-r--r--lib/api/merge_request_diffs.rb19
-rw-r--r--lib/api/merge_requests.rb66
-rw-r--r--lib/api/milestones.rb25
-rw-r--r--lib/api/notes.rb8
-rw-r--r--lib/api/notification_settings.rb9
-rw-r--r--lib/api/pipelines.rb8
-rw-r--r--lib/api/project_hooks.rb11
-rw-r--r--lib/api/project_snippets.rb18
-rw-r--r--lib/api/projects.rb39
-rw-r--r--lib/api/repositories.rb57
-rw-r--r--lib/api/runner.rb264
-rw-r--r--lib/api/runners.rb9
-rw-r--r--lib/api/services.rb62
-rw-r--r--lib/api/settings.rb17
-rw-r--r--lib/api/snippets.rb19
-rw-r--r--lib/api/subscriptions.rb3
-rw-r--r--lib/api/system_hooks.rb2
-rw-r--r--lib/api/tags.rb8
-rw-r--r--lib/api/time_tracking_endpoints.rb6
-rw-r--r--lib/api/todos.rb12
-rw-r--r--lib/api/triggers.rb91
-rw-r--r--lib/api/users.rb83
-rw-r--r--lib/api/v3/award_emoji.rb130
-rw-r--r--lib/api/v3/boards.rb23
-rw-r--r--lib/api/v3/branches.rb50
-rw-r--r--lib/api/v3/broadcast_messages.rb31
-rw-r--r--lib/api/v3/builds.rb255
-rw-r--r--lib/api/v3/commits.rb15
-rw-r--r--lib/api/v3/deploy_keys.rb2
-rw-r--r--lib/api/v3/deployments.rb43
-rw-r--r--lib/api/v3/entities.rb237
-rw-r--r--lib/api/v3/environments.rb87
-rw-r--r--lib/api/v3/files.rb2
-rw-r--r--lib/api/v3/groups.rb181
-rw-r--r--lib/api/v3/helpers.rb19
-rw-r--r--lib/api/v3/issues.rb15
-rw-r--r--lib/api/v3/labels.rb17
-rw-r--r--lib/api/v3/members.rb10
-rw-r--r--lib/api/v3/merge_request_diffs.rb44
-rw-r--r--lib/api/v3/merge_requests.rb40
-rw-r--r--lib/api/v3/milestones.rb64
-rw-r--r--lib/api/v3/notes.rb148
-rw-r--r--lib/api/v3/pipelines.rb36
-rw-r--r--lib/api/v3/project_hooks.rb106
-rw-r--r--lib/api/v3/project_snippets.rb4
-rw-r--r--lib/api/v3/projects.rb76
-rw-r--r--lib/api/v3/repositories.rb56
-rw-r--r--lib/api/v3/runners.rb65
-rw-r--r--lib/api/v3/services.rb644
-rw-r--r--lib/api/v3/settings.rb137
-rw-r--r--lib/api/v3/snippets.rb138
-rw-r--r--lib/api/v3/subscriptions.rb2
-rw-r--r--lib/api/v3/system_hooks.rb13
-rw-r--r--lib/api/v3/tags.rb22
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb116
-rw-r--r--lib/api/v3/todos.rb4
-rw-r--r--lib/api/v3/triggers.rb103
-rw-r--r--lib/api/v3/users.rb53
-rw-r--r--lib/api/v3/variables.rb29
-rw-r--r--lib/api/variables.rb8
-rw-r--r--lib/backup/database.rb70
-rw-r--r--lib/backup/files.rb8
-rw-r--r--lib/backup/manager.rb27
-rw-r--r--lib/backup/repository.rb25
-rw-r--r--lib/backup/uploads.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb18
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb64
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb11
-rw-r--r--lib/banzai/filter/image_link_filter.rb13
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb11
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb13
-rw-r--r--lib/banzai/filter/user_reference_filter.rb7
-rw-r--r--lib/banzai/filter/video_link_filter.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/banzai/reference_parser/base_parser.rb6
-rw-r--r--lib/bitbucket/connection.rb10
-rw-r--r--lib/bitbucket/error/unauthorized.rb3
-rw-r--r--lib/bitbucket/representation/repo.rb2
-rw-r--r--lib/ci/ansi2html.rb50
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/ci/api/helpers.rb6
-rw-r--r--lib/ci/api/runners.rb44
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb4
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--lib/extracts_path.rb4
-rw-r--r--lib/file_size_validator.rb4
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/allowable.rb2
-rw-r--r--lib/gitlab/auth.rb56
-rw-r--r--lib/gitlab/auth/too_many_ips.rb17
-rw-r--r--lib/gitlab/auth/unique_ips_limiter.rb43
-rw-r--r--lib/gitlab/award_emoji.rb83
-rw-r--r--lib/gitlab/badge/build/template.rb2
-rw-r--r--lib/gitlab/badge/coverage/template.rb2
-rw-r--r--lib/gitlab/changes_list.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_base.rb2
-rw-r--r--lib/gitlab/checks/change_access.rb8
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb6
-rw-r--r--lib/gitlab/ci/build/image.rb33
-rw-r--r--lib/gitlab/ci/build/step.rb46
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb8
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb13
-rw-r--r--lib/gitlab/ci/config/entry/key.rb4
-rw-r--r--lib/gitlab/ci/config/entry/node.rb8
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb4
-rw-r--r--lib/gitlab/ci/config/loader.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb12
-rw-r--r--lib/gitlab/ci/status/build/stop.rb12
-rw-r--r--lib/gitlab/ci/status/manual.rb19
-rw-r--r--lib/gitlab/ci/status/pipeline/blocked.rb23
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb3
-rw-r--r--lib/gitlab/conflict/file.rb14
-rw-r--r--lib/gitlab/conflict/file_collection.rb3
-rw-r--r--lib/gitlab/conflict/parser.rb30
-rw-r--r--lib/gitlab/conflict/resolution_error.rb3
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database.rb19
-rw-r--r--lib/gitlab/database/median.rb1
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb2
-rw-r--r--lib/gitlab/diff/line.rb6
-rw-r--r--lib/gitlab/diff/parser.rb8
-rw-r--r--lib/gitlab/diff/position.rb19
-rw-r--r--lib/gitlab/downtime_check/message.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb2
-rw-r--r--lib/gitlab/email/handler.rb2
-rw-r--r--lib/gitlab/email/receiver.rb26
-rw-r--r--lib/gitlab/email/reply_parser.rb11
-rw-r--r--lib/gitlab/emoji.rb33
-rw-r--r--lib/gitlab/etag_caching/middleware.rb66
-rw-r--r--lib/gitlab/etag_caching/store.rb32
-rw-r--r--lib/gitlab/exclusive_lease.rb2
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/blob.rb157
-rw-r--r--lib/gitlab/git/commit.rb6
-rw-r--r--lib/gitlab/git/diff.rb23
-rw-r--r--lib/gitlab/git/diff_collection.rb4
-rw-r--r--lib/gitlab/git/index.rb126
-rw-r--r--lib/gitlab/git/repository.rb135
-rw-r--r--lib/gitlab/git_access.rb10
-rw-r--r--lib/gitlab/git_ref_validator.rb3
-rw-r--r--lib/gitlab/gitaly_client.rb43
-rw-r--r--lib/gitlab/gitaly_client/commit.rb25
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb17
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb4
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb4
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb14
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/highlight.rb4
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/error.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb8
-rw-r--r--lib/gitlab/import_export/relation_factory.rb8
-rw-r--r--lib/gitlab/ldap/person.rb4
-rw-r--r--lib/gitlab/ldap/user.rb2
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb6
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb2
-rw-r--r--lib/gitlab/middleware/go.rb66
-rw-r--r--lib/gitlab/middleware/multipart.rb2
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb12
-rw-r--r--lib/gitlab/o_auth/user.rb13
-rw-r--r--lib/gitlab/optimistic_locking.rb6
-rw-r--r--lib/gitlab/prometheus.rb70
-rw-r--r--lib/gitlab/redis.rb29
-rw-r--r--lib/gitlab/reference_extractor.rb2
-rw-r--r--lib/gitlab/regex.rb11
-rw-r--r--lib/gitlab/request_context.rb21
-rw-r--r--lib/gitlab/request_profiler.rb2
-rw-r--r--lib/gitlab/route_map.rb2
-rw-r--r--lib/gitlab/saml/user.rb11
-rw-r--r--lib/gitlab/sanitizers/svg/whitelist.rb25
-rw-r--r--lib/gitlab/search_results.rb22
-rw-r--r--lib/gitlab/seeder.rb19
-rw-r--r--lib/gitlab/serializer/pagination.rb2
-rw-r--r--lib/gitlab/shell.rb8
-rw-r--r--lib/gitlab/sherlock/query.rb11
-rw-r--r--lib/gitlab/sidekiq_status.rb35
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb2
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb2
-rw-r--r--lib/gitlab/update_path_error.rb2
-rw-r--r--lib/gitlab/upgrader.rb10
-rw-r--r--lib/gitlab/url_blocker.rb59
-rw-r--r--lib/gitlab/url_sanitizer.rb10
-rw-r--r--lib/gitlab/user_access.rb14
-rw-r--r--lib/gitlab/visibility_level.rb41
-rw-r--r--lib/gitlab/workhorse.rb21
-rw-r--r--lib/mattermost/client.rb4
-rw-r--r--lib/mattermost/error.rb2
-rw-r--r--lib/mattermost/session.rb4
-rw-r--r--lib/mattermost/team.rb13
-rw-r--r--lib/omni_auth/strategies/bitbucket.rb (renamed from lib/omniauth/strategies/bitbucket.rb)0
-rw-r--r--lib/rouge/formatters/html_gitlab.rb10
-rw-r--r--lib/support/init.d/gitlab.default.example4
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-ssl10
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/dev.rake2
-rw-r--r--lib/tasks/downtime_check.rake10
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gemojione.rake93
-rw-r--r--lib/tasks/gitlab/assets.rake2
-rw-r--r--lib/tasks/gitlab/check.rake74
-rw-r--r--lib/tasks/gitlab/cleanup.rake17
-rw-r--r--lib/tasks/gitlab/db.rake8
-rw-r--r--lib/tasks/gitlab/dev.rake2
-rw-r--r--lib/tasks/gitlab/git.rake2
-rw-r--r--lib/tasks/gitlab/import.rake5
-rw-r--r--lib/tasks/gitlab/import_export.rake2
-rw-r--r--lib/tasks/gitlab/info.rake26
-rw-r--r--lib/tasks/gitlab/shell.rake6
-rw-r--r--lib/tasks/gitlab/sidekiq.rake6
-rw-r--r--lib/tasks/gitlab/task_helpers.rb55
-rw-r--r--lib/tasks/gitlab/test.rake12
-rw-r--r--lib/tasks/gitlab/track_deployment.rake4
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/gitlab/web_hook.rake6
-rw-r--r--lib/tasks/lint.rake1
-rw-r--r--lib/tasks/migrate/migrate_iids.rake2
-rw-r--r--lib/tasks/services.rake10
-rw-r--r--lib/tasks/sidekiq.rake8
-rw-r--r--lib/tasks/spec.rake30
-rw-r--r--lib/tasks/spinach.rake2
-rw-r--r--package.json30
-rw-r--r--public/ci/build-canceled.svg1
-rw-r--r--public/ci/build-failed.svg1
-rw-r--r--public/ci/build-pending.svg1
-rw-r--r--public/ci/build-running.svg1
-rw-r--r--public/ci/build-skipped.svg1
-rw-r--r--public/ci/build-success.svg1
-rw-r--r--public/ci/build-unknown.svg1
-rw-r--r--qa/.gitignore1
-rw-r--r--qa/.rspec3
-rw-r--r--qa/Dockerfile17
-rw-r--r--qa/Gemfile7
-rw-r--r--qa/Gemfile.lock61
-rw-r--r--qa/README.md18
-rwxr-xr-xqa/bin/qa7
-rwxr-xr-xqa/bin/test3
-rw-r--r--qa/qa.rb81
-rw-r--r--qa/qa/ce/strategy.rb15
-rw-r--r--qa/qa/git/repository.rb71
-rw-r--r--qa/qa/page/admin/menu.rb19
-rw-r--r--qa/qa/page/base.rb12
-rw-r--r--qa/qa/page/main/entry.rb32
-rw-r--r--qa/qa/page/main/groups.rb20
-rw-r--r--qa/qa/page/main/menu.rb46
-rw-r--r--qa/qa/page/main/projects.rb16
-rw-r--r--qa/qa/page/project/new.rb24
-rw-r--r--qa/qa/page/project/show.rb23
-rw-r--r--qa/qa/runtime/namespace.rb15
-rw-r--r--qa/qa/runtime/release.rb28
-rw-r--r--qa/qa/runtime/user.rb15
-rw-r--r--qa/qa/scenario/actable.rb23
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb31
-rw-r--r--qa/qa/scenario/template.rb16
-rw-r--r--qa/qa/scenario/test/instance.rb26
-rw-r--r--qa/qa/specs/config.rb78
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb14
-rw-r--r--qa/qa/specs/features/project/create_spec.rb19
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb57
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb39
-rw-r--r--qa/qa/specs/runner.rb15
-rw-r--r--qa/spec/runtime/release_spec.rb50
-rw-r--r--qa/spec/scenario/actable_spec.rb47
-rw-r--r--qa/spec/spec_helper.rb19
-rw-r--r--rubocop/cop/custom_error_class.rb64
-rw-r--r--rubocop/cop/gem_fetcher.rb4
-rw-r--r--rubocop/cop/migration/add_column.rb4
-rw-r--r--rubocop/cop/migration/add_column_with_default.rb2
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb2
-rw-r--r--rubocop/cop/migration/add_concurrent_index.rb34
-rw-r--r--rubocop/cop/migration/add_index.rb2
-rw-r--r--rubocop/rubocop.rb2
-rw-r--r--spec/config/mail_room_spec.rb61
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb28
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb65
-rw-r--r--spec/controllers/blob_controller_spec.rb8
-rw-r--r--spec/controllers/ci/projects_controller_spec.rb74
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb14
-rw-r--r--spec/controllers/health_check_controller_spec.rb4
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb10
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb45
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb45
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb4
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb12
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb7
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb10
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb8
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb104
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb64
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb8
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb32
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb8
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb46
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb8
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb12
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb30
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb12
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb18
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb129
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb16
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb130
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb27
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb21
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb67
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb6
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb6
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb20
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb36
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb4
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb12
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb8
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb6
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb21
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb10
-rw-r--r--spec/controllers/projects_controller_spec.rb80
-rw-r--r--spec/controllers/root_controller_spec.rb36
-rw-r--r--spec/controllers/sessions_controller_spec.rb12
-rw-r--r--spec/controllers/uploads_controller_spec.rb82
-rw-r--r--spec/factories/chat_teams.rb9
-rw-r--r--spec/factories/ci/builds.rb49
-rw-r--r--spec/factories/ci/pipelines.rb8
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/notes.rb7
-rw-r--r--spec/factories/oauth_access_grants.rb11
-rw-r--r--spec/factories/oauth_access_tokens.rb3
-rw-r--r--spec/factories/oauth_applications.rb2
-rw-r--r--spec/factories/personal_access_tokens.rb17
-rw-r--r--spec/factories/projects.rb45
-rw-r--r--spec/factories/services.rb19
-rw-r--r--spec/factories/todos.rb14
-rw-r--r--spec/factories/users.rb5
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb10
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb72
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb17
-rw-r--r--spec/features/atom/issues_spec.rb15
-rw-r--r--spec/features/atom/users_spec.rb4
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb12
-rw-r--r--spec/features/boards/boards_spec.rb189
-rw-r--r--spec/features/boards/issue_ordering_spec.rb166
-rw-r--r--spec/features/boards/modal_filter_spec.rb183
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb787
-rw-r--r--spec/features/dashboard/activity_spec.rb11
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb15
-rw-r--r--spec/features/dashboard/groups_list_spec.rb47
-rw-r--r--spec/features/dashboard/issues_spec.rb3
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb32
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb37
-rw-r--r--spec/features/dashboard_issues_spec.rb6
-rw-r--r--spec/features/environment_spec.rb225
-rw-r--r--spec/features/environments_spec.rb245
-rw-r--r--spec/features/explore/groups_list_spec.rb46
-rw-r--r--spec/features/groups/activity_spec.rb26
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb44
-rw-r--r--spec/features/groups/issues_spec.rb18
-rw-r--r--spec/features/groups/show_spec.rb24
-rw-r--r--spec/features/groups_spec.rb40
-rw-r--r--spec/features/help_pages_spec.rb26
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb2
-rw-r--r--spec/features/issuables/issuable_list_spec.rb28
-rw-r--r--spec/features/issues/award_emoji_spec.rb50
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb6
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb101
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb81
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb82
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb12
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb63
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb31
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb94
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb378
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb352
-rw-r--r--spec/features/issues/form_spec.rb24
-rw-r--r--spec/features/issues_spec.rb55
-rw-r--r--spec/features/login_spec.rb12
-rw-r--r--spec/features/markdown_spec.rb8
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb3
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb186
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb3
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb88
-rw-r--r--spec/features/merge_requests/form_spec.rb21
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb8
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb52
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/merge_requests/widget_spec.rb94
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/profile_spec.rb18
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb20
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb32
-rw-r--r--spec/features/projects/activity/rss_spec.rb29
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb97
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb107
-rw-r--r--spec/features/projects/branches_spec.rb8
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb1
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb55
-rw-r--r--spec/features/projects/commit/rss_spec.rb27
-rw-r--r--spec/features/projects/compare_spec.rb8
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb12
-rw-r--r--spec/features/projects/edit_spec.rb30
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb39
-rw-r--r--spec/features/projects/environments/environment_spec.rb220
-rw-r--r--spec/features/projects/environments/environments_spec.rb244
-rw-r--r--spec/features/projects/files/browse_files_spec.rb14
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb10
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb4
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681799 -> 679892 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb25
-rw-r--r--spec/features/projects/issues/rss_spec.rb31
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb12
-rw-r--r--spec/features/projects/main/rss_spec.rb25
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb8
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb64
-rw-r--r--spec/features/projects/new_project_spec.rb9
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb141
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb42
-rw-r--r--spec/features/projects/services/slack_service_spec.rb4
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/tree/rss_spec.rb25
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb26
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb33
-rw-r--r--spec/features/projects/wiki/user_views_project_wiki_page_spec.rb44
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/features/search_spec.rb13
-rw-r--r--spec/features/security/project/internal_access_spec.rb14
-rw-r--r--spec/features/security/project/private_access_spec.rb14
-rw-r--r--spec/features/security/project/public_access_spec.rb14
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb27
-rw-r--r--spec/features/tags/master_views_tags_spec.rb10
-rw-r--r--spec/features/todos/todos_sorting_spec.rb8
-rw-r--r--spec/features/todos/todos_spec.rb99
-rw-r--r--spec/features/triggers_spec.rb169
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb26
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb24
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb22
-rw-r--r--spec/features/user_callout_spec.rb37
-rw-r--r--spec/features/users/rss_spec.rb22
-rw-r--r--spec/finders/issues_finder_spec.rb35
-rw-r--r--spec/finders/members_finder_spec.rb22
-rw-r--r--spec/finders/notes_finder_spec.rb2
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb196
-rw-r--r--spec/finders/pipelines_finder_spec.rb4
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v3/issues.json77
-rw-r--r--spec/fixtures/api/schemas/public_api/v3/merge_requests.json89
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json76
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json88
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json (renamed from spec/fixtures/api/schemas/user/login.json)0
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json (renamed from spec/fixtures/api/schemas/user/public.json)0
-rw-r--r--spec/fixtures/markdown.md.erb5
-rw-r--r--spec/helpers/application_helper_spec.rb10
-rw-r--r--spec/helpers/auth_helper_spec.rb2
-rw-r--r--spec/helpers/blob_helper_spec.rb14
-rw-r--r--spec/helpers/ci_status_helper_spec.rb7
-rw-r--r--spec/helpers/emails_helper_spec.rb32
-rw-r--r--spec/helpers/events_helper_spec.rb15
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb22
-rw-r--r--spec/helpers/issuables_helper_spec.rb2
-rw-r--r--spec/helpers/issues_helper_spec.rb38
-rw-r--r--spec/helpers/milestones_helper_spec.rb56
-rw-r--r--spec/helpers/projects_helper_spec.rb40
-rw-r--r--spec/helpers/rss_helper_spec.rb20
-rw-r--r--spec/helpers/submodule_helper_spec.rb44
-rw-r--r--spec/helpers/todos_helper_spec.rb57
-rw-r--r--spec/helpers/version_check_helper_spec.rb34
-rw-r--r--spec/initializers/6_validations_spec.rb80
-rw-r--r--spec/initializers/8_metrics_spec.rb16
-rw-r--r--spec/initializers/doorkeeper_spec.rb71
-rw-r--r--spec/initializers/metrics_spec.rb16
-rw-r--r--spec/initializers/secret_token_spec.rb25
-rw-r--r--spec/initializers/trusted_proxies_spec.rb4
-rw-r--r--spec/javascripts/.eslintrc3
-rw-r--r--spec/javascripts/abuse_reports_spec.js (renamed from spec/javascripts/abuse_reports_spec.js.es6)0
-rw-r--r--spec/javascripts/activities_spec.js (renamed from spec/javascripts/activities_spec.js.es6)0
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js58
-rw-r--r--spec/javascripts/awards_handler_spec.js202
-rw-r--r--spec/javascripts/behaviors/bind_in_out_spec.js189
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js107
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js119
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js93
-rw-r--r--spec/javascripts/boards/board_card_spec.js168
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js190
-rw-r--r--spec/javascripts/boards/boards_store_spec.js232
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6158
-rw-r--r--spec/javascripts/boards/issue_card_spec.js (renamed from spec/javascripts/boards/issue_card_spec.js.es6)0
-rw-r--r--spec/javascripts/boards/issue_spec.js98
-rw-r--r--spec/javascripts/boards/issue_spec.js.es682
-rw-r--r--spec/javascripts/boards/list_spec.js109
-rw-r--r--spec/javascripts/boards/list_spec.js.es687
-rw-r--r--spec/javascripts/boards/mock_data.js (renamed from spec/javascripts/boards/mock_data.js.es6)0
-rw-r--r--spec/javascripts/boards/modal_store_spec.js (renamed from spec/javascripts/boards/modal_store_spec.js.es6)0
-rw-r--r--spec/javascripts/bootstrap_jquery_spec.js42
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js (renamed from spec/javascripts/bootstrap_linked_tabs_spec.js.es6)0
-rw-r--r--spec/javascripts/build_spec.js177
-rw-r--r--spec/javascripts/build_spec.js.es6183
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js89
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js.es692
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js100
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js.es6105
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es633
-rw-r--r--spec/javascripts/commits_spec.js (renamed from spec/javascripts/commits_spec.js.es6)0
-rw-r--r--spec/javascripts/datetime_utility_spec.js (renamed from spec/javascripts/datetime_utility_spec.js.es6)0
-rw-r--r--spec/javascripts/diff_comments_store_spec.js133
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es6124
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js47
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es666
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js22
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es621
-rw-r--r--spec/javascripts/environments/environment_item_spec.js212
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es6210
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js59
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es647
-rw-r--r--spec/javascripts/environments/environment_spec.js178
-rw-r--r--spec/javascripts/environments/environment_spec.js.es6178
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js34
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es628
-rw-r--r--spec/javascripts/environments/environment_table_spec.js34
-rw-r--r--spec/javascripts/environments/environment_table_spec.js.es630
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js24
-rw-r--r--spec/javascripts/environments/environments_store_spec.js58
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es658
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js202
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js.es6202
-rw-r--r--spec/javascripts/environments/mock_data.js86
-rw-r--r--spec/javascripts/environments/mock_data.js.es692
-rw-r--r--spec/javascripts/extensions/array_spec.js22
-rw-r--r--spec/javascripts/extensions/array_spec.js.es645
-rw-r--r--spec/javascripts/extensions/element_spec.js.es638
-rw-r--r--spec/javascripts/extensions/jquery_spec.js42
-rw-r--r--spec/javascripts/extensions/object_spec.js.es625
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js71
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js.es675
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js310
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js.es6290
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js101
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es659
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js262
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es667
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6)0
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6)0
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js600
-rw-r--r--spec/javascripts/fixtures/ajax_loading_spinner.html.haml2
-rw-r--r--spec/javascripts/fixtures/branches.rb2
-rw-r--r--spec/javascripts/fixtures/builds.rb2
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js4
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml12
-rw-r--r--spec/javascripts/fixtures/issues.rb2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb2
-rw-r--r--spec/javascripts/fixtures/project_branches.json5
-rw-r--r--spec/javascripts/fixtures/projects.json18
-rw-r--r--spec/javascripts/fixtures/projects.rb2
-rw-r--r--spec/javascripts/fixtures/target_branch_dropdown.html.haml28
-rw-r--r--spec/javascripts/fixtures/todos.rb4
-rw-r--r--spec/javascripts/fixtures/user_callout.html.haml2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js (renamed from spec/javascripts/gfm_auto_complete_spec.js.es6)0
-rw-r--r--spec/javascripts/gl_dropdown_spec.js (renamed from spec/javascripts/gl_dropdown_spec.js.es6)0
-rw-r--r--spec/javascripts/gl_emoji_spec.js399
-rw-r--r--spec/javascripts/gl_field_errors_spec.js (renamed from spec/javascripts/gl_field_errors_spec.js.es6)0
-rw-r--r--spec/javascripts/gl_form_spec.js (renamed from spec/javascripts/gl_form_spec.js.es6)0
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js8
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js3
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js20
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js11
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js.es69
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js (renamed from spec/javascripts/helpers/class_spec_helper_spec.js.es6)0
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js52
-rw-r--r--spec/javascripts/issuable_spec.js (renamed from spec/javascripts/issuable_spec.js.es6)0
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js (renamed from spec/javascripts/issuable_time_tracker_spec.js.es6)0
-rw-r--r--spec/javascripts/issue_spec.js58
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js (renamed from spec/javascripts/labels_issue_sidebar_spec.js.es6)0
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js (renamed from spec/javascripts/lib/utils/common_utils_spec.js.es6)0
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js110
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es650
-rw-r--r--spec/javascripts/line_highlighter_spec.js18
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js72
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es651
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js75
-rw-r--r--spec/javascripts/monitoring/prometheus_mock_data.js1014
-rw-r--r--spec/javascripts/new_branch_spec.js1
-rw-r--r--spec/javascripts/pager_spec.js90
-rw-r--r--spec/javascripts/pipelines_spec.js (renamed from spec/javascripts/pipelines_spec.js.es6)0
-rw-r--r--spec/javascripts/polyfills/element_spec.js36
-rw-r--r--spec/javascripts/pretty_time_spec.js (renamed from spec/javascripts/pretty_time_spec.js.es6)0
-rw-r--r--spec/javascripts/project_title_spec.js2
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js8
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js (renamed from spec/javascripts/signin_tabs_memoizer_spec.js.es6)0
-rw-r--r--spec/javascripts/smart_interval_spec.js (renamed from spec/javascripts/smart_interval_spec.js.es6)0
-rw-r--r--spec/javascripts/subbable_resource_spec.js (renamed from spec/javascripts/subbable_resource_spec.js.es6)0
-rw-r--r--spec/javascripts/test_bundle.js52
-rw-r--r--spec/javascripts/user_callout_spec.js57
-rw-r--r--spec/javascripts/version_check_image_spec.js33
-rw-r--r--spec/javascripts/visibility_select_spec.js (renamed from spec/javascripts/visibility_select_spec.js.es6)0
-rw-r--r--spec/javascripts/vue_pipelines_index/async_button_spec.js93
-rw-r--r--spec/javascripts/vue_pipelines_index/pipeline_url_spec.js100
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js62
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js40
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_store_spec.js72
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js130
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js.es6131
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js87
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es687
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js63
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js.es664
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js146
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es6166
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb112
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb6
-rw-r--r--spec/lib/bitbucket/collection_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb4
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb88
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb4
-rw-r--r--spec/lib/expand_variables_spec.rb31
-rw-r--r--spec/lib/extracts_path_spec.rb4
-rw-r--r--spec/lib/git_ref_validator_spec.rb5
-rw-r--r--spec/lib/gitlab/auth/unique_ips_limiter_spec.rb57
-rw-r--r--spec/lib/gitlab/auth_spec.rb78
-rw-r--r--spec/lib/gitlab/award_emoji_spec.rb41
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb15
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/build/image_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/build/step_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/factory_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb79
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/paths_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb6
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb89
-rw-r--r--spec/lib/gitlab/database_spec.rb35
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb12
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb48
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb163
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb185
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb37
-rw-r--r--spec/lib/gitlab/git/index_spec.rb220
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb277
-rw-r--r--spec/lib/gitlab/git_access_spec.rb26
-rw-r--r--spec/lib/gitlab/git_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_spec.rb58
-rw-r--r--spec/lib/gitlab/gitaly_client/notifications_spec.rb20
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb120
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb48
-rw-r--r--spec/lib/gitlab/highlight_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/file_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project.json69
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb19
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml10
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb38
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb95
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb16
-rw-r--r--spec/lib/gitlab/optimistic_locking_spec.rb19
-rw-r--r--spec/lib/gitlab/prometheus_spec.rb143
-rw-r--r--spec/lib/gitlab/redis_spec.rb59
-rw-r--r--spec/lib/gitlab/regex_spec.rb4
-rw-r--r--spec/lib/gitlab/request_context_spec.rb30
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb21
-rw-r--r--spec/lib/gitlab/serializer/ci/variables_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb26
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb3
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb3
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb3
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb31
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb30
-rw-r--r--spec/lib/gitlab/utils_spec.rb4
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb21
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb54
-rw-r--r--spec/lib/mattermost/command_spec.rb6
-rw-r--r--spec/lib/mattermost/team_spec.rb29
-rw-r--r--spec/mailers/emails/builds_spec.rb64
-rw-r--r--spec/mailers/notify_spec.rb242
-rw-r--r--spec/migrations/migrate_build_events_to_pipeline_events_spec.rb74
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb4
-rw-r--r--spec/migrations/rename_more_reserved_project_names_spec.rb47
-rw-r--r--spec/models/ability_spec.rb6
-rw-r--r--spec/models/abuse_report_spec.rb2
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb40
-rw-r--r--spec/models/blob_spec.rb25
-rw-r--r--spec/models/chat_team_spec.rb15
-rw-r--r--spec/models/ci/build_spec.rb151
-rw-r--r--spec/models/ci/pipeline_spec.rb76
-rw-r--r--spec/models/ci/pipeline_status_spec.rb173
-rw-r--r--spec/models/ci/runner_spec.rb8
-rw-r--r--spec/models/ci/stage_spec.rb17
-rw-r--r--spec/models/ci/trigger_spec.rb72
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb47
-rw-r--r--spec/models/commit_status_spec.rb27
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb5
-rw-r--r--spec/models/concerns/has_status_spec.rb54
-rw-r--r--spec/models/concerns/issuable_spec.rb50
-rw-r--r--spec/models/concerns/milestoneish_spec.rb26
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb204
-rw-r--r--spec/models/concerns/routable_spec.rb4
-rw-r--r--spec/models/concerns/uniquify_spec.rb33
-rw-r--r--spec/models/cycle_analytics/production_spec.rb5
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb7
-rw-r--r--spec/models/environment_spec.rb90
-rw-r--r--spec/models/external_issue_spec.rb8
-rw-r--r--spec/models/global_milestone_spec.rb63
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/issue_spec.rb50
-rw-r--r--spec/models/members/project_member_spec.rb8
-rw-r--r--spec/models/merge_request_diff_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb95
-rw-r--r--spec/models/namespace_spec.rb18
-rw-r--r--spec/models/note_spec.rb12
-rw-r--r--spec/models/personal_access_token_spec.rb60
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb2
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb4
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb111
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb77
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb10
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb42
-rw-r--r--spec/models/project_services/irker_service_spec.rb4
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb32
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb16
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb5
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb104
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb74
-rw-r--r--spec/models/project_wiki_spec.rb21
-rw-r--r--spec/models/repository_spec.rb146
-rw-r--r--spec/models/route_spec.rb12
-rw-r--r--spec/models/upload_spec.rb151
-rw-r--r--spec/models/user_spec.rb56
-rw-r--r--spec/policies/base_policy_spec.rb10
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb103
-rw-r--r--spec/policies/user_policy_spec.rb37
-rw-r--r--spec/presenters/projects/settings/deploy_keys_presenter_spec.rb66
-rw-r--r--spec/requests/api/access_requests_spec.rb4
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb2
-rw-r--r--spec/requests/api/award_emoji_spec.rb66
-rw-r--r--spec/requests/api/boards_spec.rb3
-rw-r--r--spec/requests/api/branches_spec.rb194
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb7
-rw-r--r--spec/requests/api/builds_spec.rb475
-rw-r--r--spec/requests/api/commit_statuses_spec.rb72
-rw-r--r--spec/requests/api/commits_spec.rb110
-rw-r--r--spec/requests/api/deploy_keys_spec.rb2
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb34
-rw-r--r--spec/requests/api/environments_spec.rb41
-rw-r--r--spec/requests/api/files_spec.rb209
-rw-r--r--spec/requests/api/groups_spec.rb36
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb39
-rw-r--r--spec/requests/api/issues_spec.rb273
-rw-r--r--spec/requests/api/jobs_spec.rb480
-rw-r--r--spec/requests/api/labels_spec.rb15
-rw-r--r--spec/requests/api/members_spec.rb22
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb32
-rw-r--r--spec/requests/api/merge_requests_spec.rb210
-rw-r--r--spec/requests/api/milestones_spec.rb89
-rw-r--r--spec/requests/api/notes_spec.rb10
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb22
-rw-r--r--spec/requests/api/pipelines_spec.rb1
-rw-r--r--spec/requests/api/project_hooks_spec.rb16
-rw-r--r--spec/requests/api/project_snippets_spec.rb20
-rw-r--r--spec/requests/api/projects_spec.rb269
-rw-r--r--spec/requests/api/repositories_spec.rb100
-rw-r--r--spec/requests/api/runner_spec.rb1072
-rw-r--r--spec/requests/api/runners_spec.rb15
-rw-r--r--spec/requests/api/services_spec.rb2
-rw-r--r--spec/requests/api/session_spec.rb18
-rw-r--r--spec/requests/api/settings_spec.rb18
-rw-r--r--spec/requests/api/snippets_spec.rb14
-rw-r--r--spec/requests/api/system_hooks_spec.rb2
-rw-r--r--spec/requests/api/tags_spec.rb4
-rw-r--r--spec/requests/api/templates_spec.rb8
-rw-r--r--spec/requests/api/todos_spec.rb10
-rw-r--r--spec/requests/api/triggers_spec.rb174
-rw-r--r--spec/requests/api/users_spec.rb213
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb299
-rw-r--r--spec/requests/api/v3/boards_spec.rb34
-rw-r--r--spec/requests/api/v3/branches_spec.rb112
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb34
-rw-r--r--spec/requests/api/v3/builds_spec.rb489
-rw-r--r--spec/requests/api/v3/commits_spec.rb15
-rw-r--r--spec/requests/api/v3/deployments_spec.rb71
-rw-r--r--spec/requests/api/v3/environments_spec.rb165
-rw-r--r--spec/requests/api/v3/files_spec.rb41
-rw-r--r--spec/requests/api/v3/groups_spec.rb565
-rw-r--r--spec/requests/api/v3/issues_spec.rb17
-rw-r--r--spec/requests/api/v3/labels_spec.rb29
-rw-r--r--spec/requests/api/v3/members_spec.rb2
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb50
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb11
-rw-r--r--spec/requests/api/v3/milestones_spec.rb239
-rw-r--r--spec/requests/api/v3/notes_spec.rb433
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb203
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb216
-rw-r--r--spec/requests/api/v3/projects_spec.rb41
-rw-r--r--spec/requests/api/v3/repositories_spec.rb222
-rw-r--r--spec/requests/api/v3/runners_spec.rb154
-rw-r--r--spec/requests/api/v3/services_spec.rb24
-rw-r--r--spec/requests/api/v3/settings_spec.rb65
-rw-r--r--spec/requests/api/v3/snippets_spec.rb187
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb16
-rw-r--r--spec/requests/api/v3/tags_spec.rb22
-rw-r--r--spec/requests/api/v3/templates_spec.rb8
-rw-r--r--spec/requests/api/v3/triggers_spec.rb232
-rw-r--r--spec/requests/api/v3/users_spec.rb77
-rw-r--r--spec/requests/api/variables_spec.rb3
-rw-r--r--spec/requests/ci/api/builds_spec.rb45
-rw-r--r--spec/requests/ci/api/runners_spec.rb5
-rw-r--r--spec/requests/git_http_spec.rb12
-rw-r--r--spec/requests/lfs_http_spec.rb178
-rw-r--r--spec/requests/openid_connect_spec.rb134
-rw-r--r--spec/routing/openid_connect_spec.rb30
-rw-r--r--spec/routing/project_routing_spec.rb21
-rw-r--r--spec/rubocop/cop/custom_error_class_spec.rb111
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb41
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb4
-rw-r--r--spec/services/boards/issues/move_service_spec.rb18
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb73
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb18
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb50
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb703
-rw-r--r--spec/services/ci/register_build_service_spec.rb178
-rw-r--r--spec/services/ci/register_job_service_spec.rb223
-rw-r--r--spec/services/ci/retry_build_service_spec.rb47
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb101
-rw-r--r--spec/services/create_deployment_service_spec.rb17
-rw-r--r--spec/services/git_push_service_spec.rb7
-rw-r--r--spec/services/groups/create_service_spec.rb22
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb56
-rw-r--r--spec/services/issues/create_service_spec.rb85
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb106
-rw-r--r--spec/services/issues/update_service_spec.rb16
-rw-r--r--spec/services/merge_requests/build_service_spec.rb5
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb10
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb37
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb106
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb13
-rw-r--r--spec/services/notes/create_service_spec.rb50
-rw-r--r--spec/services/notification_service_spec.rb138
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/import_service_spec.rb20
-rw-r--r--spec/services/projects/update_pages_service_spec.rb22
-rw-r--r--spec/services/projects/upload_service_spec.rb14
-rw-r--r--spec/services/protected_branches/create_service_spec.rb4
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb39
-rw-r--r--spec/services/system_hooks_service_spec.rb3
-rw-r--r--spec/services/system_note_service_spec.rb49
-rw-r--r--spec/services/todo_service_spec.rb30
-rw-r--r--spec/services/users/destroy_spec.rb48
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb74
-rw-r--r--spec/simplecov_env.rb6
-rw-r--r--spec/spec_helper.rb13
-rw-r--r--spec/support/api/issues_resolving_discussions_shared_examples.rb15
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb28
-rw-r--r--spec/support/api/v3/time_tracking_shared_examples.rb128
-rw-r--r--spec/support/api_helpers.rb4
-rw-r--r--spec/support/capybara.rb11
-rw-r--r--spec/support/carrierwave.rb4
-rw-r--r--spec/support/cycle_analytics_helpers.rb14
-rw-r--r--spec/support/dropzone_helper.rb37
-rw-r--r--spec/support/features/resolving_discussions_in_issues_shared_examples.rb41
-rw-r--r--spec/support/features/rss_shared_examples.rb23
-rw-r--r--spec/support/filtered_search_helpers.rb43
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb13
-rw-r--r--spec/support/javascript_fixtures_helpers.rb2
-rw-r--r--spec/support/jira_service_helper.rb2
-rw-r--r--spec/support/json_response_helpers.rb9
-rw-r--r--spec/support/kubernetes_helpers.rb13
-rw-r--r--spec/support/login_helpers.rb11
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/matchers/email_matchers.rb5
-rw-r--r--spec/support/matchers/gitaly_matchers.rb3
-rw-r--r--spec/support/matchers/markdown_matchers.rb7
-rw-r--r--spec/support/project_features_apply_to_issuables_shared_examples.rb2
-rw-r--r--spec/support/prometheus_helpers.rb117
-rw-r--r--spec/support/repo_helpers.rb16
-rw-r--r--spec/support/seed_helper.rb6
-rw-r--r--spec/support/seed_repo.rb76
-rw-r--r--spec/support/select2_helper.rb2
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/stub_gitlab_calls.rb2
-rw-r--r--spec/support/test_env.rb10
-rw-r--r--spec/support/unique_ip_check_shared_examples.rb62
-rw-r--r--spec/support/update_invalid_issuable.rb57
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb25
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb37
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb7
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb7
-rw-r--r--spec/uploaders/file_uploader_spec.rb63
-rw-r--r--spec/uploaders/records_uploads_spec.rb97
-rw-r--r--spec/uploaders/uploader_helper_spec.rb37
-rw-r--r--spec/views/ci/status/_badge.html.haml_spec.rb89
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb42
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb30
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb19
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb25
-rw-r--r--spec/workers/build_email_worker_spec.rb36
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb4
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb20
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb57
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb129
-rw-r--r--spec/workers/system_hook_push_worker_spec.rb19
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb11
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb19
-rw-r--r--vendor/assets/javascripts/g.bar.js674
-rw-r--r--vendor/assets/javascripts/g.raphael.js861
-rw-r--r--vendor/assets/javascripts/jquery.highlight.js53
-rw-r--r--vendor/assets/javascripts/raphael.js8239
-rw-r--r--vendor/gitignore/Android.gitignore3
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore5
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore1
-rw-r--r--vendor/gitignore/Global/SBT.gitignore3
-rw-r--r--vendor/gitignore/Java.gitignore1
-rw-r--r--vendor/gitignore/Maven.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore2
-rw-r--r--vendor/gitignore/Objective-C.gitignore4
-rw-r--r--vendor/gitignore/PlayFramework.gitignore1
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Scala.gitignore21
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/Symfony.gitignore4
-rw-r--r--vendor/gitignore/TeX.gitignore9
-rw-r--r--vendor/gitignore/VisualStudio.gitignore11
-rw-r--r--vendor/gitlab-ci-yml/Android.gitlab-ci.yml51
-rw-r--r--vendor/gitlab-ci-yml/Bash.gitlab-ci.yml35
-rw-r--r--vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml1
-rw-r--r--vendor/gitlab-ci-yml/Django.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/LICENSE2
-rw-r--r--vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml78
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml11
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml92
-rw-r--r--vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml92
-rw-r--r--vendor/gitlab-ci-yml/PHP.gitlab-ci.yml33
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml14
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml19
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml21
-rw-r--r--vendor/licenses.csv945
-rw-r--r--yarn.lock159
4580 files changed, 81412 insertions, 54398 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000000..ee4c391da30
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+ "presets": [
+ ["latest", { "es2015": { "modules": false } }],
+ "stage-2"
+ ],
+ "env": {
+ "coverage": {
+ "plugins": [
+ ["istanbul", {
+ "exclude": [
+ "app/assets/javascripts/droplab/**/*",
+ "spec/javascripts/**/*"
+ ]
+ }],
+ ["transform-define", {
+ "process.env.BABEL_ENV": "coverage"
+ }]
+ ]
+ }
+ }
+}
diff --git a/.eslintrc b/.eslintrc
index 0fcd866778f..b0ae2a31919 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -23,7 +23,7 @@
}
},
"rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+ "filenames/match-regex": [2, "^[a-z0-9_]+$"],
"no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
diff --git a/.flayignore b/.flayignore
index fc64b0b5892..47597025115 100644
--- a/.flayignore
+++ b/.flayignore
@@ -2,3 +2,4 @@
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb
+app/models/concerns/relative_positioning.rb
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 70cce05d2b5..00000000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.js.es6 gitlab-language=javascript
diff --git a/.gitignore b/.gitignore
index 5e9f19d8319..51b4d06b01b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ eslint-report.html
/builds/*
/shared/*
/.gitlab_workhorse_secret
+/webpack-report/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 433b3119fba..34c10b3b77f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,8 +7,6 @@ cache:
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
- # retry tests only in CI environment
- RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
SETUP_DB: "true"
@@ -60,7 +58,7 @@ stages:
<<: *dedicated-runner
<<: *use-db
script:
- - JOB_NAME=( $CI_BUILD_NAME )
+ - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -69,16 +67,18 @@ stages:
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
+ when: always
paths:
- - knapsack/
- coverage/
+ - knapsack/
+ - tmp/capybara/
.spinach-knapsack: &spinach-knapsack
stage: test
<<: *dedicated-runner
<<: *use-db
script:
- - JOB_NAME=( $CI_BUILD_NAME )
+ - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -87,9 +87,11 @@ stages:
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
+ when: always
paths:
- - knapsack/
- coverage/
+ - knapsack/
+ - tmp/capybara/
# Prepare and merge knapsack tests
@@ -178,7 +180,7 @@ spinach 9 10: *spinach-knapsack
<<: *dedicated-runner
stage: test
script:
- - bundle exec $CI_BUILD_NAME
+ - bundle exec $CI_JOB_NAME
rubocop:
<<: *ruby-static-analysis
@@ -209,7 +211,7 @@ rake ee_compat_check:
- ee_compat_check/repo/
- vendor/ruby
artifacts:
- name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+ name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
expire_in: 10d
paths:
@@ -222,6 +224,14 @@ rake db:migrate:reset:
script:
- bundle exec rake db:migrate:reset
+rake db:rollback:
+ stage: test
+ <<: *use-db
+ <<: *dedicated-runner
+ script:
+ - bundle exec rake db:rollback STEP=120
+ - bundle exec rake db:migrate
+
rake db:seed_fu:
stage: test
<<: *use-db
@@ -240,6 +250,25 @@ rake db:seed_fu:
paths:
- log/development.log
+rake gitlab:assets:compile:
+ stage: test
+ <<: *dedicated-runner
+ dependencies: []
+ variables:
+ NODE_ENV: "production"
+ RAILS_ENV: "production"
+ SETUP_DB: "false"
+ USE_DB: "false"
+ SKIP_STORAGE_VALIDATION: "true"
+ WEBPACK_REPORT: "true"
+ script:
+ - bundle exec rake yarn:install gitlab:assets:compile
+ artifacts:
+ name: webpack-report
+ expire_in: 31d
+ paths:
+ - webpack-report/
+
rake karma:
cache:
paths:
@@ -248,6 +277,8 @@ rake karma:
stage: test
<<: *use-db
<<: *dedicated-runner
+ variables:
+ BABEL_ENV: "coverage"
script:
- bundle exec rake karma
artifacts:
@@ -281,7 +312,7 @@ bundler:audit:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+ - "bundle exec bundle-audit check --update"
migration paths:
stage: test
@@ -301,7 +332,7 @@ migration paths:
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- - git checkout $CI_BUILD_REF
+ - git checkout $CI_COMMIT_SHA
- source scripts/prepare_build.sh
- bundle exec rake db:migrate
@@ -339,7 +370,7 @@ lint:javascript:report:
stage: post-test
before_script: []
script:
- - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
+ - find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
@@ -360,12 +391,13 @@ trigger_docs:
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
+ - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
+ - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
# Notify slack in the end
-
notify:slack:
stage: post-test
<<: *dedicated-runner
@@ -373,7 +405,7 @@ notify:slack:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
+ - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
@@ -388,6 +420,7 @@ pages:
dependencies:
- coverage
- rake karma
+ - rake gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
@@ -395,11 +428,13 @@ pages:
- mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
+ - mv webpack-report/ public/webpack-report/ || true
artifacts:
paths:
- public
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
# Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future.
@@ -416,3 +451,4 @@ cache gems:
- vendor/cache
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index ea895ee6275..2636010e2fb 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -5,3 +5,13 @@
### Proposal
### Links / references
+
+### Documentation blurb
+
+(Write the start of the documentation of this feature here, include:
+
+1. Why should someone use it; what's the underlying problem.
+2. What is the solution.
+3. How does someone use this
+
+During implementation, this can then be copied and used as a starter for the documentation.)
diff --git a/.rubocop.yml b/.rubocop.yml
index a836b469cc7..fa1370ea1f3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -22,14 +22,12 @@ AllCops:
- 'db/fixtures/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- - 'lib/backup/**/*'
- - 'lib/ci/backup/**/*'
- - 'lib/tasks/**/*'
- - 'lib/ci/migrate/**/*'
- - 'lib/email_validator.rb'
- - 'lib/gitlab/upgrader.rb'
- - 'lib/gitlab/seeder.rb'
- 'generator_templates/**/*'
+ - 'builds/**/*'
+
+# Gems in consecutive lines should be alphabetically sorted
+Bundler/OrderedGems:
+ Enabled: false
# Style #######################################################################
@@ -54,6 +52,11 @@ Style/AlignArray:
Style/AlignHash:
Enabled: true
+# Here we check if the parameters on a multi-line method call or
+# definition are aligned.
+Style/AlignParameters:
+ Enabled: false
+
# Whether `and` and `or` are banned only in conditionals (conditionals)
# or completely (always).
Style/AndOr:
@@ -83,15 +86,24 @@ Style/BeginBlock:
Style/BlockComments:
Enabled: true
-# Put end statement of multiline block on its own line.
-Style/BlockEndNewline:
- Enabled: true
-
# Avoid using {...} for multi-line blocks (multiline chaining is # always
# ugly). Prefer {...} over do...end for single-line blocks.
Style/BlockDelimiters:
Enabled: true
+# Put end statement of multiline block on its own line.
+Style/BlockEndNewline:
+ Enabled: true
+
+ # This cop checks for braces around the last parameter in a method call
+# if the last parameter is a hash.
+Style/BracesAroundHashParameters:
+ Enabled: false
+
+# This cop checks for uses of the case equality operator(===).
+Style/CaseEquality:
+ Enabled: false
+
# Indentation of when in a case/when/[else/]end.
Style/CaseIndentation:
Enabled: true
@@ -110,7 +122,7 @@ Style/ClassAndModuleChildren:
# Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
Style/ClassCheck:
- Enabled: false
+ Enabled: true
# Use self when defining module/class methods.
Style/ClassMethods:
@@ -120,10 +132,26 @@ Style/ClassMethods:
Style/ClassVars:
Enabled: true
+# This cop checks for methods invoked via the :: operator instead
+# of the . operator (like FileUtils::rmdir instead of FileUtils.rmdir).
+Style/ColonMethodCall:
+ Enabled: true
+
+# This cop checks that comment annotation keywords are written according
+# to guidelines.
+Style/CommentAnnotation:
+ Enabled: false
+
# Indentation of comments.
Style/CommentIndentation:
Enabled: true
+# Check for `if` and `case` statements where each branch is used for
+# assignment to the same variable when using the return of the
+# condition can be used instead.
+Style/ConditionalAssignment:
+ Enabled: true
+
# Constants should use SCREAMING_SNAKE_CASE.
Style/ConstantName:
Enabled: true
@@ -136,13 +164,19 @@ Style/DefWithParentheses:
Style/Documentation:
Enabled: false
+# This cop checks for uses of double negation (!!) to convert something
+# to a boolean value. As this is both cryptic and usually redundant, it
+# should be avoided.
+Style/DoubleNegation:
+ Enabled: false
+
# Align elses and elsifs correctly.
Style/ElseAlignment:
Enabled: true
# Use empty lines between defs.
Style/EmptyLineBetweenDefs:
- Enabled: false
+ Enabled: true
# Don't use several empty lines in a row.
Style/EmptyLines:
@@ -160,14 +194,14 @@ Style/EmptyLinesAroundBlockBody:
Style/EmptyLinesAroundClassBody:
Enabled: true
-# Keeps track of empty lines around module bodies.
-Style/EmptyLinesAroundModuleBody:
- Enabled: true
-
# Keeps track of empty lines around method bodies.
Style/EmptyLinesAroundMethodBody:
Enabled: true
+# Keeps track of empty lines around module bodies.
+Style/EmptyLinesAroundModuleBody:
+ Enabled: true
+
# Avoid the use of END blocks.
Style/EndBlock:
Enabled: true
@@ -200,24 +234,28 @@ Style/For:
# Checks if there is a magic comment to enforce string literals
Style/FrozenStringLiteralComment:
Enabled: false
+
# Do not introduce global variables.
Style/GlobalVars:
Enabled: true
+ Exclude:
+ - 'lib/backup/**/*'
+ - 'lib/tasks/**/*'
# Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
# over 1.8 syntax `{ :a => 1, :b => 2 }`.
Style/HashSyntax:
Enabled: true
-# Do not use if x; .... Use the ternary operator instead.
-Style/IfWithSemicolon:
- Enabled: true
-
# Checks that conditional statements do not have an identical line at the
# end of each branch, which can validly be moved out of the conditional.
Style/IdenticalConditionalBranches:
Enabled: true
+# Do not use if x; .... Use the ternary operator instead.
+Style/IfWithSemicolon:
+ Enabled: true
+
# Checks the indentation of the first line of the right-hand-side of a
# multi-line assignment.
Style/IndentAssignment:
@@ -258,7 +296,7 @@ Style/ModuleFunction:
# Checks that the closing brace in an array literal is either on the same line
# as the last array element, or a new line.
Style/MultilineArrayBraceLayout:
- Enabled: false
+ Enabled: true
EnforcedStyle: symmetrical
# Avoid multi-line chains of blocks.
@@ -272,7 +310,7 @@ Style/MultilineBlockLayout:
# Checks that the closing brace in a hash literal is either on the same line as
# the last hash element, or a new line.
Style/MultilineHashBraceLayout:
- Enabled: false
+ Enabled: true
EnforcedStyle: symmetrical
# Do not use then for multi-line if/unless.
@@ -304,6 +342,14 @@ Style/MultilineOperationIndentation:
Style/MultilineTernaryOperator:
Enabled: true
+# This cop checks whether some constant value isn't a
+# mutable literal (e.g. array or hash).
+Style/MutableConstant:
+ Enabled: true
+ Exclude:
+ - 'db/migrate/**/*'
+ - 'db/post_migrate/**/*'
+
# Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf:
Enabled: true
@@ -406,6 +452,10 @@ Style/SpaceBeforeComment:
Style/SpaceBeforeSemicolon:
Enabled: true
+# Checks for spaces inside square brackets.
+Style/SpaceInsideBrackets:
+ Enabled: true
+
# Use spaces inside hash literal braces - or don't.
Style/SpaceInsideHashLiteralBraces:
Enabled: true
@@ -442,6 +492,10 @@ Style/Tab:
Style/TrailingBlankLines:
Enabled: true
+# This cop checks for trailing comma in array and hash literals.
+Style/TrailingCommaInLiteral:
+ Enabled: false
+
# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
Enabled: true
@@ -477,7 +531,7 @@ Style/WhileUntilModifier:
# Use %w or %W for arrays of words.
Style/WordArray:
- Enabled: false
+ Enabled: true
# Metrics #####################################################################
@@ -487,6 +541,10 @@ Metrics/AbcSize:
Enabled: true
Max: 60
+# This cop checks if the length of a block exceeds some maximum value.
+Metrics/BlockLength:
+ Enabled: false
+
# Avoid excessive block nesting.
Metrics/BlockNesting:
Enabled: true
@@ -526,20 +584,21 @@ Metrics/PerceivedComplexity:
# Lint ########################################################################
-# Checks for useless access modifiers.
-Lint/UselessAccessModifier:
- Enabled: true
-
-# Checks for attempts to use `private` or `protected` to set the visibility
-# of a class method, which does not work.
-Lint/IneffectiveAccessModifier:
- Enabled: false
-
# Checks for ambiguous operators in the first argument of a method invocation
# without parentheses.
Lint/AmbiguousOperator:
Enabled: true
+# This cop checks for ambiguous regexp literals in the first argument of
+# a method invocation without parentheses.
+Lint/AmbiguousRegexpLiteral:
+ Enabled: false
+
+# This cop checks for assignments in the conditions of
+# if/while/until.
+Lint/AssignmentInCondition:
+ Enabled: false
+
# Align block ends correctly.
Lint/BlockAlignment:
Enabled: true
@@ -593,10 +652,6 @@ Lint/EndInMethod:
Lint/EnsureReturn:
Enabled: true
-# The use of eval represents a serious security risk.
-Lint/Eval:
- Enabled: true
-
# Catches floating-point literals too large or small for Ruby to represent.
Lint/FloatOutOfRange:
Enabled: true
@@ -605,11 +660,20 @@ Lint/FloatOutOfRange:
Lint/FormatParameterMismatch:
Enabled: true
+# This cop checks for *rescue* blocks with no body.
+Lint/HandleExceptions:
+ Enabled: false
+
# Checks for adjacent string literals on the same line, which could better be
# represented as a single string literal.
Lint/ImplicitStringConcatenation:
Enabled: true
+# Checks for attempts to use `private` or `protected` to set the visibility
+# of a class method, which does not work.
+Lint/IneffectiveAccessModifier:
+ Enabled: false
+
# Checks for invalid character literals with a non-escaped whitespace
# character.
Lint/InvalidCharacterLiteral:
@@ -623,6 +687,10 @@ Lint/LiteralInCondition:
Lint/LiteralInInterpolation:
Enabled: true
+# This cop checks for uses of *begin...end while/until something*.
+Lint/Loop:
+ Enabled: false
+
# Do not use nested method definitions.
Lint/NestedMethodDefinition:
Enabled: true
@@ -652,6 +720,11 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
+# This cop looks for use of the same name as outer local variables
+# for block arguments or block local variables.
+Lint/ShadowingOuterLocalVariable:
+ Enabled: false
+
# Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
Enabled: true
@@ -660,16 +733,36 @@ Lint/StringConversionInInterpolation:
Lint/UnderscorePrefixedVariableName:
Enabled: true
+# This cop checks for using Fixnum or Bignum constant
+Lint/UnifiedInteger:
+ Enabled: true
+
# Checks for rubocop:disable comments that can be removed.
# Note: this cop is not disabled when disabling all cops.
# It must be explicitly disabled.
Lint/UnneededDisable:
Enabled: false
+# This cop checks for unneeded usages of splat expansion
+Lint/UnneededSplatExpansion:
+ Enabled: false
+
# Unreachable code.
Lint/UnreachableCode:
Enabled: true
+# This cop checks for unused block arguments.
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+# This cop checks for unused method arguments.
+Lint/UnusedMethodArgument:
+ Enabled: false
+
+# Checks for useless access modifiers.
+Lint/UselessAccessModifier:
+ Enabled: true
+
# Checks for useless assignment to a local variable.
Lint/UselessAssignment:
Enabled: true
@@ -709,6 +802,22 @@ Performance/LstripRstrip:
Performance/RangeInclude:
Enabled: true
+# This cop identifies the use of a `&block` parameter and `block.call`
+# where `yield` would do just as well.
+Performance/RedundantBlockCall:
+ Enabled: true
+
+# This cop identifies use of `Regexp#match` or `String#match in a context
+# where the integral return value of `=~` would do just as well.
+Performance/RedundantMatch:
+ Enabled: true
+
+# This cop identifies places where `Hash#merge!` can be replaced by
+# `Hash#[]=`.
+Performance/RedundantMerge:
+ Enabled: true
+ MaxKeyValuePairs: 1
+
# Use `sort` instead of `sort_by { |x| x }`.
Performance/RedundantSortBy:
Enabled: true
@@ -728,6 +837,17 @@ Performance/StringReplacement:
Performance/TimesMap:
Enabled: true
+# Security ####################################################################
+
+# This cop checks for the use of JSON class methods which have potential
+# security issues.
+Security/JSONLoad:
+ Enabled: true
+
+# This cop checks for the use of *Kernel#eval*.
+Security/Eval:
+ Enabled: true
+
# Rails #######################################################################
# Enables Rails cops.
@@ -746,8 +866,19 @@ Rails/Date:
# Prefer delegate method for delegations.
Rails/Delegate:
+ Enabled: true
+
+# This cop checks dynamic `find_by_*` methods.
+Rails/DynamicFindBy:
Enabled: false
+# This cop enforces that 'exit' calls are not used within a rails app.
+Rails/Exit:
+ Enabled: true
+ Exclude:
+ - lib/gitlab/upgrader.rb
+ - 'lib/backup/**/*'
+
# Prefer `find_by` over `where.first`.
Rails/FindBy:
Enabled: true
@@ -760,9 +891,25 @@ Rails/FindEach:
Rails/HasAndBelongsToMany:
Enabled: true
+# This cop is used to identify usages of http methods like `get`, `post`,
+# `put`, `patch` without the usage of keyword arguments in your tests and
+# change them to use keyword args.
+Rails/HttpPositionalArguments:
+ Enabled: false
+
# Checks for calls to puts, print, etc.
Rails/Output:
Enabled: true
+ Exclude:
+ - lib/gitlab/seeder.rb
+ - lib/gitlab/upgrader.rb
+ - 'lib/backup/**/*'
+ - 'lib/tasks/**/*'
+
+# This cop checks for the use of output safety calls like html_safe and
+# raw.
+Rails/OutputSafety:
+ Enabled: false
# Checks for incorrect grammar when using methods like `3.day.ago`.
Rails/PluralizationGrammar:
@@ -776,6 +923,14 @@ Rails/ReadWriteAttribute:
Rails/ScopeArgs:
Enabled: true
+# This cop checks for the use of Time methods without zone.
+Rails/TimeZone:
+ Enabled: false
+
+# This cop checks for the use of old-style attribute validation macros.
+Rails/Validation:
+ Enabled: true
+
# RSpec #######################################################################
# Check that instances are not being stubbed globally.
@@ -784,7 +939,7 @@ RSpec/AnyInstance:
# Check for expectations where `be(...)` can replace `eql(...)`.
RSpec/BeEql:
- Enabled: false
+ Enabled: true
# Check that the first argument to the top level describe is the tested class or
# module.
@@ -833,21 +988,51 @@ RSpec/Focus:
RSpec/InstanceVariable:
Enabled: false
+# Checks for `subject` definitions that come after `let` definitions.
+RSpec/LeadingSubject:
+ Enabled: false
+
+# Checks unreferenced `let!` calls being used for test setup.
+RSpec/LetSetup:
+ Enabled: false
+
+# Check that chains of messages are not being stubbed.
+RSpec/MessageChain:
+ Enabled: false
+
+# Checks that message expectations are set using spies.
+RSpec/MessageSpies:
+ Enabled: false
+
# Checks for multiple top-level describes.
RSpec/MultipleDescribes:
Enabled: false
+# Checks if examples contain too many `expect` calls.
+RSpec/MultipleExpectations:
+ Enabled: false
+
+# Checks for explicitly referenced test subjects.
+RSpec/NamedSubject:
+ Enabled: false
+
+# Checks for nested example groups.
+RSpec/NestedGroups:
+ Enabled: false
+
# Enforces the usage of the same method on all negative message expectations.
RSpec/NotToNot:
EnforcedStyle: not_to
Enabled: true
-# Prefer using verifying doubles over normal doubles.
-RSpec/VerifiedDoubles:
+# Check for repeated description strings in example groups.
+RSpec/RepeatedDescription:
Enabled: false
-# Custom ######################################################################
+# Checks for stubbed test subjects.
+RSpec/SubjectStub:
+ Enabled: false
-# Disallow the `git` and `github` arguments in the Gemfile.
-GemFetcher:
- Enabled: true
+# Prefer using verifying doubles over normal doubles.
+RSpec/VerifiedDoubles:
+ Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index a5b4d2f5b02..c24142c0a11 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,79 +1,13 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0.
+# on 2017-02-22 13:02:35 -0600 using RuboCop version 0.47.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 27
-# Configuration parameters: Include.
-# Include: **/Gemfile, **/gems.rb
-Bundler/OrderedGems:
- Enabled: false
-
-# Offense count: 175
-Lint/AmbiguousRegexpLiteral:
- Enabled: false
-
-# Offense count: 53
-# Configuration parameters: AllowSafeAssignment.
-Lint/AssignmentInCondition:
- Enabled: false
-
-# Offense count: 20
-Lint/HandleExceptions:
- Enabled: false
-
-# Offense count: 1
-Lint/Loop:
- Enabled: false
-
-# Offense count: 27
-Lint/ShadowingOuterLocalVariable:
- Enabled: false
-
-# Offense count: 10
-# Cop supports --auto-correct.
-Lint/UnifiedInteger:
- Enabled: false
-
-# Offense count: 21
-# Cop supports --auto-correct.
-Lint/UnneededSplatExpansion:
- Enabled: false
-
-# Offense count: 82
-# Cop supports --auto-correct.
-# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
-Lint/UnusedBlockArgument:
- Enabled: false
-
-# Offense count: 173
-# Cop supports --auto-correct.
-# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
-Lint/UnusedMethodArgument:
- Enabled: false
-
-# Offense count: 93
-# Configuration parameters: CountComments.
-Metrics/BlockLength:
- Enabled: false
-
-# Offense count: 3
-# Cop supports --auto-correct.
-Performance/RedundantBlockCall:
- Enabled: false
-
-# Offense count: 5
-# Cop supports --auto-correct.
-Performance/RedundantMatch:
- Enabled: false
-
-# Offense count: 32
-# Cop supports --auto-correct.
-# Configuration parameters: MaxKeyValuePairs.
-Performance/RedundantMerge:
+# Offense count: 51
+RSpec/BeforeAfterAll:
Enabled: false
# Offense count: 15
@@ -81,7 +15,11 @@ Performance/RedundantMerge:
RSpec/EmptyExampleGroup:
Enabled: false
-# Offense count: 58
+# Offense count: 1
+RSpec/ExpectOutput:
+ Enabled: false
+
+# Offense count: 63
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
@@ -93,147 +31,59 @@ RSpec/HookArgument:
RSpec/ImplicitExpect:
Enabled: false
-# Offense count: 237
-RSpec/LeadingSubject:
- Enabled: false
-
-# Offense count: 253
-RSpec/LetSetup:
- Enabled: false
-
-# Offense count: 13
-RSpec/MessageChain:
- Enabled: false
-
-# Offense count: 479
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: have_received, receive
-RSpec/MessageSpies:
- Enabled: false
-
-# Offense count: 3036
-RSpec/MultipleExpectations:
- Enabled: false
-
-# Offense count: 2133
-RSpec/NamedSubject:
- Enabled: false
-
-# Offense count: 1974
-# Configuration parameters: MaxNesting.
-RSpec/NestedGroups:
+# Offense count: 36
+RSpec/RepeatedExample:
Enabled: false
-# Offense count: 32
-RSpec/RepeatedDescription:
+# Offense count: 34
+RSpec/ScatteredSetup:
Enabled: false
# Offense count: 1
RSpec/SingleArgumentMessageChain:
Enabled: false
-# Offense count: 133
-RSpec/SubjectStub:
- Enabled: false
-
-# Offense count: 104
-# Cop supports --auto-correct.
-# Configuration parameters: Whitelist.
-# Whitelist: find_by_sql
-Rails/DynamicFindBy:
- Enabled: false
-
-# Offense count: 932
-# Cop supports --auto-correct.
-# Configuration parameters: Include.
-# Include: spec/**/*, test/**/*
-Rails/HttpPositionalArguments:
- Enabled: false
-
-# Offense count: 55
-Rails/OutputSafety:
+# Offense count: 163
+Rails/FilePath:
Enabled: false
-# Offense count: 182
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: strict, flexible
-Rails/TimeZone:
- Enabled: false
-
-# Offense count: 15
-# Cop supports --auto-correct.
+# Offense count: 2
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
-Rails/Validation:
+# Include: db/migrate/*.rb
+Rails/ReversibleMigration:
Enabled: false
-# Offense count: 8
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect.
-Security/JSONLoad:
+# Offense count: 278
+# Configuration parameters: Blacklist.
+# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
+Rails/SkipsModelValidations:
Enabled: false
-# Offense count: 346
+# Offense count: 7
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
-# SupportedStyles: with_first_parameter, with_fixed_indentation
-Style/AlignParameters:
+Security/YAMLLoad:
Enabled: false
-# Offense count: 54
+# Offense count: 55
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 358
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: braces, no_braces, context_dependent
-Style/BracesAroundHashParameters:
- Enabled: false
-
-# Offense count: 6
-Style/CaseEquality:
- Enabled: false
-
-# Offense count: 37
-# Cop supports --auto-correct.
-Style/ColonMethodCall:
- Enabled: false
-
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: Keywords.
-# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
-Style/CommentAnnotation:
- Enabled: false
-
-# Offense count: 29
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
-# SupportedStyles: assign_to_condition, assign_inside_condition
-Style/ConditionalAssignment:
- Enabled: false
-
-# Offense count: 1210
+# Offense count: 1304
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
Style/DotPosition:
Enabled: false
-# Offense count: 18
-Style/DoubleNegation:
- Enabled: false
-
-# Offense count: 7
+# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 24
+# Offense count: 25
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -245,14 +95,14 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 57
+# Offense count: 56
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Enabled: false
-# Offense count: 147
+# Offense count: 184
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
@@ -264,50 +114,50 @@ Style/ExtraSpacing:
Style/FormatString:
Enabled: false
-# Offense count: 238
+# Offense count: 268
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 11
+# Offense count: 14
Style/IfInsideElse:
Enabled: false
-# Offense count: 173
+# Offense count: 179
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 55
+# Offense count: 57
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
-# Offense count: 101
+# Offense count: 120
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
-# Offense count: 41
+# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Enabled: false
-# Offense count: 5
+# Offense count: 7
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Enabled: false
-# Offense count: 19
+# Offense count: 22
# Cop supports --auto-correct.
-Style/MethodCallParentheses:
+Style/MethodCallWithoutArgsParentheses:
Enabled: false
# Offense count: 9
@@ -319,61 +169,49 @@ Style/MethodMissing:
Style/MultilineIfModifier:
Enabled: false
-# Offense count: 179
-# Cop supports --auto-correct.
-Style/MutableConstant:
- Enabled: false
-
-# Offense count: 8
+# Offense count: 22
# Cop supports --auto-correct.
Style/NestedParenthesizedCalls:
Enabled: false
-# Offense count: 13
+# Offense count: 17
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
-# Offense count: 19
+# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 19
+# Offense count: 77
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
-# Offense count: 34
+# Offense count: 36
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 417
+# Offense count: 477
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Enabled: false
-# Offense count: 10
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: lower_case_q, upper_case_q
-Style/PercentQLiterals:
- Enabled: false
-
-# Offense count: 13
+# Offense count: 14
# Cop supports --auto-correct.
Style/PerlBackrefs:
Enabled: false
-# Offense count: 64
+# Offense count: 72
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -381,7 +219,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 33
+# Offense count: 39
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
@@ -393,7 +231,7 @@ Style/PreferredHashMethods:
Style/Proc:
Enabled: false
-# Offense count: 50
+# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
@@ -405,30 +243,30 @@ Style/RaiseArgs:
Style/RedundantBegin:
Enabled: false
-# Offense count: 29
+# Offense count: 32
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 11
+# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleReturnValues.
Style/RedundantReturn:
Enabled: false
-# Offense count: 359
+# Offense count: 365
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 105
+# Offense count: 108
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 19
+# Offense count: 22
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
@@ -438,19 +276,13 @@ Style/RescueModifier:
Style/SelfAssignment:
Enabled: false
-# Offense count: 2
-# Configuration parameters: Methods.
-# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]}
-Style/SingleLineBlockParams:
- Enabled: false
-
# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: AllowIfMethodIsEmpty.
Style/SingleLineMethods:
Enabled: false
-# Offense count: 138
+# Offense count: 155
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
@@ -463,26 +295,22 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 37
+# Offense count: 38
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: require_no_space, require_space
Style/SpaceInLambdaLiteral:
Enabled: false
-# Offense count: 174
+# Offense count: 203
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 115
-# Cop supports --auto-correct.
-Style/SpaceInsideBrackets:
- Enabled: false
-
-# Offense count: 77
+# Offense count: 91
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
@@ -492,21 +320,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 53
+# Offense count: 55
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 25
+# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 54
+# Offense count: 57
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@@ -520,27 +348,20 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
-# Offense count: 36
+# Offense count: 43
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Enabled: false
-# Offense count: 150
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
-Style/TrailingCommaInLiteral:
- Enabled: false
-
-# Offense count: 7
+# Offense count: 13
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
Style/TrailingUnderscoreVariable:
Enabled: false
-# Offense count: 67
+# Offense count: 70
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
@@ -552,12 +373,12 @@ Style/TrailingWhitespace:
Style/TrivialAccessors:
Enabled: false
-# Offense count: 2
+# Offense count: 6
# Cop supports --auto-correct.
Style/UnlessElse:
Enabled: false
-# Offense count: 17
+# Offense count: 22
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58b8cf2ad83..da1898e3770 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,230 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.17.4 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
+## 8.17.3 (2017-03-07)
+
+- Fix the redirect to custom home page URL. !9518
+- Fix broken migration when upgrading straight to 8.17.1. !9613
+- Make projects dropdown only show projects you are a member of. !9614
+- Fix creating a file in an empty repo using the API. !9632
+- Don't copy tooltip when copying GFM.
+- Fix cherry-picking or reverting through an MR.
+
+## 8.17.2 (2017-03-01)
+
+- Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602
+
+## 8.17.1 (2017-02-28)
+
+- Replace setInterval with setTimeout to prevent highly frequent requests. !9271 (Takuya Noguchi)
+- Disable unused tags count cache for Projects, Builds and Runners.
+- Spam check and reCAPTCHA improvements.
+- Allow searching issues for strings containing colons.
+- Disabled tooltip on add issues button in usse boards.
+- Fixed commit search UI.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+- Disable invalid service templates.
+- Use default branch as target_branch when parameter is missing.
+- Upgrade GitLab Pages to v0.3.2.
+- Add performance query regression fix for !9088 affecting #27267.
+- Chat slash commands show labels correctly.
+
+## 8.17.0 (2017-02-22)
+
+- API: Fix file downloading. !0 (8267)
+- Changed composer installer script in the CI PHP example doc. !4342 (Jeffrey Cafferata)
+- Display fullscreen button on small screens. !5302 (winniehell)
+- Add system hook for when a project is updated (other than rename/transfer). !5711 (Tommy Beadle)
+- Fix notifications when set at group level. !6813 (Alexandre Maia)
+- Project labels can now be promoted to group labels. !7242 (Olaf Tomalka)
+- use webpack to bundle frontend assets and use karma for frontend testing. !7288
+- Adds back ability to stop all environments. !7379
+- Added labels empty state. !7443
+- Add ability to define a coverage regex in the .gitlab-ci.yml. !7447 (Leandro Camargo)
+- Disable automatic login after clicking email confirmation links. !7472
+- Search feature: redirects to commit page if query is commit sha and only commit found. !8028 (YarNayar)
+- Create a TODO for user who set auto-merge when a build fails, merge conflict occurs. !8056 (twonegatives)
+- Don't group issues by project on group-level and dashboard issue indexes. !8111 (Bernardo Castro)
+- Mark MR as WIP when pushing WIP commits. !8124 (Jurre Stender @jurre)
+- Flag multiple empty lines in eslint, fix offenses. !8137
+- Add sorting pipeline for a commit. !8319 (Takuya Noguchi)
+- Adds service trigger events to api. !8324
+- Update pipeline and commit links when CI status is updated. !8351
+- Hide version check image if there is no internet connection. !8355 (Ken Ding)
+- Prevent removal of input fields if it is the parent dropdown element. !8397
+- Introduce maximum session time for terminal websocket connection. !8413
+- Allow creating protected branches when user can merge to such branch. !8458
+- Refactor MergeRequests::BuildService. !8462 (Rydkin Maxim)
+- Added GitLab Pages to CE. !8463
+- Support notes when a project is not specified (personal snippet notes). !8468
+- Use warning icon in mini-graph if stage passed conditionally. !8503
+- Don’t count tasks that are not defined as list items correctly. !8526
+- Reformat messages ChatOps. !8528
+- Copy commit SHA to clipboard. !8547
+- Improve button accessibility on pipelines page. !8561
+- Display project ID in project settings. !8572 (winniehell)
+- PlantUML support for Markdown. !8588 (Horacio Sanson)
+- Fix reply by email without sub-addressing for some clients from Microsoft and Apple. !8620
+- Fix nested tasks in ordered list. !8626
+- Fix Sort by Recent Sign-in in Admin Area. !8637 (Poornima M)
+- Avoid repeated dashes in $CI_ENVIRONMENT_SLUG. !8638
+- Only show Merge Request button when user can create a MR. !8639
+- Prevent copying of line numbers in parallel diff view. !8706
+- Improve build policy and access abilities. !8711
+- API: Remove /projects/:id/keys/.. endpoints. !8716 (Robert Schilling)
+- API: Remove deprecated 'expires_at' from project snippets. !8723 (Robert Schilling)
+- Add `copy` backup strategy to combat file changed errors. !8728
+- adds avatar for discussion note. !8734
+- Add link verification to badge partial in order to render a badge without a link. !8740
+- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752
+- prevent diff unfolding link from appearing when there are no more lines to show. !8761
+- Redesign searchbar in admin project list. !8776
+- Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere. !8787
+- dismiss sidebar on repo buttons click. !8798 (Adam Pahlevi)
+- fixed small mini pipeline graph line glitch. !8804
+- Make all system notes lowercase. !8807
+- Support unauthenticated LFS object downloads for public projects. !8824 (Ben Boeckel)
+- Add read-only full_path and full_name attributes to Group API. !8827
+- allow relative url change without recompiling frontend assets. !8831
+- Use vue.js Pipelines table in commit and merge request view. !8844
+- Use reCaptcha when an issue is identified as a spam. !8846
+- resolve deprecation warnings. !8855 (Adam Pahlevi)
+- Cop for gem fetched from a git source. !8856 (Adam Pahlevi)
+- Remove flash warning from login page. !8864 (Gerald J. Padilla)
+- Adds documentation for how to use Vue.js. !8866
+- Add 'View on [env]' link to blobs and individual files in diffs. !8867
+- Replace word user with member. !8872
+- Change the reply shortcut to focus the field even without a selection. !8873 (Brian Hall)
+- Unify MR diff file button style. !8874
+- Unify projects search by removing /projects/:search endpoint. !8877
+- Fix disable storing of sensitive information when importing a new repo. !8885 (Bernard Pietraga)
+- Fix pipeline graph vertical spacing in Firefox and Safari. !8886
+- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891
+- Fix Ctrl+Click support for Todos and Merge Request page tabs. !8898
+- Fix wrong call to ProjectCacheWorker.perform. !8910
+- Don't perform Devise trackable updates on blocked User records. !8915
+- Add ability to export project inherited group members to Import/Export. !8923
+- replace `find_with_namespace` with `find_by_full_path`. !8949 (Adam Pahlevi)
+- Fixes flickering of avatar border in mention dropdown. !8950
+- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956
+- Fix deleting projects with pipelines and builds. !8960
+- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko)
+- Ensure autogenerated title does not cause failing spec. !8963 (brian m. carlson)
+- Update doc for enabling or disabling GitLab CI. !8965 (Takuya Noguchi)
+- Remove deprecated MR and Issue endpoints and preserve V3 namespace. !8967
+- Fixed "substract" typo on /help/user/project/slash_commands. !8976 (Jason Aquino)
+- Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context. !8981
+- use babel to transpile all non-vendor javascript assets regardless of file extension. !8988
+- Fix MR widget url. !8989
+- Fixes hover cursor on pipeline pagenation. !9003
+- Layer award emoji dropdown over the right sidebar. !9004
+- Do not display deploy keys in user's own ssh keys list. !9024
+- upgrade babel 5.8.x to babel 6.22.x. !9072
+- upgrade to webpack v2.2. !9078
+- Trigger autocomplete after selecting a slash command. !9117
+- Add space between text and loading icon in Megre Request Widget. !9119
+- Fix job to pipeline renaming. !9147
+- Replace static fixture for merge_request_tabs_spec.js. !9172 (winniehell)
+- Replace static fixture for right_sidebar_spec.js. !9211 (winniehell)
+- Show merge errors in merge request widget. !9229
+- Increase process_commit queue weight from 2 to 3. !9326 (blackst0ne)
+- Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb.
+- Force new password after password reset via API. (George Andrinopoulos)
+- Allows to search within project by commit hash. (YarNayar)
+- Show organisation membership and delete comment on smaller viewports, plus change comment author name to username.
+- Remove turbolinks.
+- Convert pipeline action icons to svg to have them propperly positioned.
+- Remove rogue scrollbars for issue comments with inline elements.
+- Align Segoe UI label text.
+- Color + and - signs in diffs to increase code legibility.
+- Fix tab index order on branch commits list page. (Ryan Harris)
+- Add hover style to copy icon on commit page header. (Ryan Harris)
+- Remove hover animation from row elements.
+- Improve pipeline status icon linking in widgets.
+- Fix commit title bar and repository view copy clipboard button order on last commit in repository view.
+- Fix mini-pipeline stage tooltip text wrapping.
+- Updated builds info link on the project settings page. (Ryan Harris)
+- 27240 Make progress bars consistent.
+- Only render hr when user can't archive project.
+- 27352-search-label-filter-header.
+- Include :author, :project, and :target in Event.with_associations.
+- Don't instantiate AR objects in Event.in_projects.
+- Don't capitalize environment name in show page.
+- Update and pin the `jwt` gem to ~> 1.5.6.
+- Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles.
+- Give ci status text on pipeline graph a better font-weight.
+- Add default labels to bulk assign dropdowns.
+- Only return target project's comments for a commit.
+- Fixes Pipelines table is not showing branch name for commit.
+- Fix regression where cmd-click stopped working for todos and merge request tabs.
+- Fix stray pipelines API request when showing MR.
+- Fix Merge request pipelines displays JSON.
+- Fix current build arrow indicator.
+- Fix contribution activity alignment.
+- Show Pipeline(not Job) in MR desktop notification.
+- Fix tooltips in mini pipeline graph.
+- Display loading indicator when filtering ref switcher dropdown.
+- Show pipeline graph in MR widget if there are any stages.
+- Fix icon colors in merge request widget mini graph.
+- Improve blockquote formatting in notification emails.
+- Adds container to tooltip in order to make it work with overflow:hidden in parent element.
+- Restore pagination to admin abuse reports.
+- Ensure export files are removed after a namespace is deleted.
+- Add `y` keyboard shortcut to move to file permalink.
+- Adds /target_branch slash command functionality for merge requests. (YarNayar)
+- Patch Asciidocs rendering to block XSS.
+- contribution calendar scrolls from right to left.
+- Copying a rendered issue/comment will paste into GFM textareas as actual GFM.
+- Don't delete assigned MRs/issues when user is deleted.
+- Remove new branch button for confidential issues.
+- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
+- Don't connect in Gitlab::Database.adapter_name.
+- Prevent users from creating notes on resources they can't access.
+- Ignore encrypted attributes in Import/Export.
+- Change rspec test to guarantee window is resized before visiting page.
+- Prevent users from deleting system deploy keys via the project deploy key API.
+- Fix XSS vulnerability in SVG attachments.
+- Make MR-review-discussions more reliable.
+- fix incorrect sidekiq concurrency count in admin background page. (wendy0402)
+- Make notification_service spec DRYer by making test reusable. (YarNayar)
+- Redirect http://someproject.git to http://someproject. (blackst0ne)
+- Fixed group label links in issue/merge request sidebar.
+- Improve gl.utils.handleLocationHash tests.
+- Fixed Issuable sidebar not closing on smaller/mobile sized screens.
+- Resets assignee dropdown when sidebar is open.
+- Disallow system notes for closed issuables.
+- Fix timezone on issue boards due date.
+- Remove unused js response from refs controller.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Fixed merge requests tab extra margin when fixed to window.
+- Patch XSS vulnerability in RDOC support.
+- Refresh authorizations when transferring projects.
+- Remove issue and MR counts from labels index.
+- Don't use backup Active Record connections for Sidekiq.
+- Add index to ci_trigger_requests for commit_id.
+- Add indices to improve loading of labels page.
+- Reduced query count for snippet search.
+- Update GitLab Pages to v0.3.1.
+- Upgrade omniauth gem to 1.3.2.
+- Remove deprecated GitlabCiService.
+- Requeue pending deletion projects.
+
+## 8.16.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
+## 8.16.7 (2017-02-27)
+
+- No changes.
+- No changes.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+
## 8.16.6 (2017-02-17)
- API: Fix file downloading. !0 (8267)
@@ -197,6 +421,11 @@ entry.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
+## 8.15.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
## 8.15.7 (2017-02-15)
- No changes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de32a953f63..a285e8ab74f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,32 +1,48 @@
+## Contributor license agreement
+
+By submitting code as an individual you agree to the
+[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
+By submitting code as an entity you agree to the
+[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+
+_This notice should stay as the first item in the CONTRIBUTING.MD file._
+
+---
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+- [Contributor license agreement](#contributor-license-agreement)
- [Contribute to GitLab](#contribute-to-gitlab)
- - [Contributor license agreement](#contributor-license-agreement)
- - [Security vulnerability disclosure](#security-vulnerability-disclosure)
- - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
- - [Helping others](#helping-others)
- - [I want to contribute!](#i-want-to-contribute)
- - [Implement design & UI elements](#implement-design-ui-elements)
- - [Issue tracker](#issue-tracker)
- - [Feature proposals](#feature-proposals)
- - [Issue tracker guidelines](#issue-tracker-guidelines)
- - [Issue weight](#issue-weight)
- - [Regression issues](#regression-issues)
- - [Technical debt](#technical-debt)
- - [Stewardship](#stewardship)
- - [Merge requests](#merge-requests)
- - [Merge request guidelines](#merge-request-guidelines)
- - [Contribution acceptance criteria](#contribution-acceptance-criteria)
- - [Changes for Stable Releases](#changes-for-stable-releases)
- - [Definition of done](#definition-of-done)
- - [Style guides](#style-guides)
- - [Code of conduct](#code-of-conduct)
+- [Security vulnerability disclosure](#security-vulnerability-disclosure)
+- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
+- [Helping others](#helping-others)
+- [I want to contribute!](#i-want-to-contribute)
+- [Implement design & UI elements](#implement-design-ui-elements)
+- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
+ - [Retrospective](#retrospective)
+ - [Kickoff](#kickoff)
+- [Issue tracker](#issue-tracker)
+ - [Feature proposals](#feature-proposals)
+ - [Issue tracker guidelines](#issue-tracker-guidelines)
+ - [Issue weight](#issue-weight)
+ - [Regression issues](#regression-issues)
+ - [Technical debt](#technical-debt)
+ - [Stewardship](#stewardship)
+- [Merge requests](#merge-requests)
+ - [Merge request guidelines](#merge-request-guidelines)
+ - [Contribution acceptance criteria](#contribution-acceptance-criteria)
+- [Changes for Stable Releases](#changes-for-stable-releases)
+- [Definition of done](#definition-of-done)
+- [Style guides](#style-guides)
+- [Code of conduct](#code-of-conduct)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-# Contribute to GitLab
+---
+
+## Contribute to GitLab
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
@@ -41,13 +57,6 @@ operates please see [the GitLab contributing process](PROCESS.md).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
-## Contributor license agreement
-
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
-
## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to
@@ -69,6 +78,13 @@ towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language
for audiences of all ages.
+If a contributor is no longer actively working on a submitted merge request
+we can decide that the merge request will be finished by one of our
+[Merge request coaches][team] or close the merge request. We make this decision
+based on how important the change is for our product vision. If a Merge request
+coach is going to finish the merge request we assign the
+~"coach will finish" label.
+
## Helping others
Please help other GitLab users when you can. The channels people will reach out
@@ -85,6 +101,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
+## Workflow labels
+
+Labelling issues is described in the [GitLab Inc engineering workflow].
+
## Implement design & UI elements
Please see the [UX Guide for GitLab].
@@ -290,10 +310,13 @@ request is as follows:
1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
-1. If you have multiple commits please combine them into one commit by
- [squashing them][git-squash]
+1. If you have multiple commits please combine them into a few logically
+ organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch
+1. Leave the approvals settings as they are:
+ 1. Your merge request needs at least 1 approval
+ 1. You don't have to select any approvers
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
used to achieve it.
@@ -336,13 +359,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k
For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback
-on your merge request feel free to mention one of the Merge Marshalls in the
-[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/).
+on your merge request feel free to mention someone from the [core team] or one
+of the [Merge request coaches][team].
Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
+### Getting your merge request reviewed, approved, and merged
+
+There are a few rules to get your merge request accepted:
+
+1. Your merge request should only be **merged by a [maintainer][team]**.
+ 1. If your merge request includes only backend changes [^1], it must be
+ **approved by a [backend maintainer][team]**.
+ 1. If your merge request includes only frontend changes [^1], it must be
+ **approved by a [frontend maintainer][team]**.
+ 1. If your merge request includes frontend and backend changes [^1], it must
+ be approved by a frontend **and** a backend maintainer.
+1. To lower the amount of merge requests maintainers need to review, you can
+ ask or assign any [reviewers][team] for a first review.
+ 1. If you need some guidance (e.g. it's your first merge request), feel free
+ to ask one of the [Merge request coaches][team].
+ 1. The reviewer will assign the merge request to a maintainer once the
+ reviewer is satisfied with the state of the merge request.
+
### Contribution acceptance criteria
1. The change is as small as possible
@@ -365,6 +406,12 @@ When having your code reviewed and when reviewing merge requests please take the
1. Contains functionality we think other users will benefit from too
1. Doesn't add configuration options or settings options since they complicate
making and testing future changes
+1. Changes do not adversely degrade performance.
+ - Avoid repeated polling of endpoints that require a significant amount of overhead
+ - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
+ - Avoid repeated access of filesystem
+1. If you need polling to support real-time features, please use
+ [polling with ETag caching][polling-etag].
1. Changes after submitting the merge request should be in separate commits
(no squashing). If necessary, you will be asked to squash when the review is
over, before merging.
@@ -400,6 +447,7 @@ the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. Unit and integration tests that pass on the CI server
+1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-styleguide] in the /doc directory
1. Changelog entry added
1. Reviewed and any concerns are addressed
@@ -426,7 +474,7 @@ merge request:
1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
Important sections include [Source Code Layout][rss-source] and
[Naming][rss-naming]. Use:
- - multi-line method chaining style **Option B**: dot `.` on previous line
+ - multi-line method chaining style **Option A**: dot `.` on the second line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide]
@@ -480,6 +528,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
[core team]: https://about.gitlab.com/core-team/
+[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
@@ -504,3 +553,9 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
+[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
+
+[^1]: Specs other than JavaScript specs are considered backend code. Haml
+ changes are considered backend code if they include Ruby code other than just
+ pure HTML.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
new file mode 100644
index 00000000000..0d91a54c7d4
--- /dev/null
+++ b/GITALY_SERVER_VERSION
@@ -0,0 +1 @@
+0.3.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 9e11b32fcaa..1d0ba9ea182 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.3.1
+0.4.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 627a3f43a64..0062ac97180 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.1.1
+5.0.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index f0bb29e7638..347f5833ee6 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.3.0
+1.4.1
diff --git a/Gemfile b/Gemfile
index 01861f1ffac..6af27ce0f3e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
# Authentication libraries
-gem 'devise', '~> 4.2'
-gem 'doorkeeper', '~> 4.2.0'
-gem 'omniauth', '~> 1.3.2'
-gem 'omniauth-auth0', '~> 1.4.1'
-gem 'omniauth-azure-oauth2', '~> 0.0.6'
-gem 'omniauth-cas3', '~> 1.1.2'
-gem 'omniauth-facebook', '~> 4.0.0'
-gem 'omniauth-github', '~> 1.1.1'
-gem 'omniauth-gitlab', '~> 1.0.2'
+gem 'devise', '~> 4.2'
+gem 'doorkeeper', '~> 4.2.0'
+gem 'doorkeeper-openid_connect', '~> 1.1.0'
+gem 'omniauth', '~> 1.4.2'
+gem 'omniauth-auth0', '~> 1.4.1'
+gem 'omniauth-azure-oauth2', '~> 0.0.6'
+gem 'omniauth-cas3', '~> 1.1.2'
+gem 'omniauth-facebook', '~> 4.0.0'
+gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
-gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
+gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
-gem 'omniauth-saml', '~> 1.7.0'
-gem 'omniauth-shibboleth', '~> 1.2.0'
-gem 'omniauth-twitter', '~> 1.2.0'
-gem 'omniauth_crowd', '~> 2.2.0'
-gem 'omniauth-authentiq', '~> 0.3.0'
-gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt', '~> 1.5.6'
+gem 'omniauth-saml', '~> 1.7.0'
+gem 'omniauth-shibboleth', '~> 1.2.0'
+gem 'omniauth-twitter', '~> 1.2.0'
+gem 'omniauth_crowd', '~> 2.2.0'
+gem 'omniauth-authentiq', '~> 0.3.0'
+gem 'rack-oauth2', '~> 1.2.1'
+gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.18.0'
+gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
-gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
+gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
gem 'kaminari', '~> 0.17.0'
@@ -79,7 +80,7 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 0.10.0'
+gem 'carrierwave', '~> 0.11.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup', '~> 1.5.1'
-gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 4.2'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.1'
+gem 'redcarpet', '~> 3.4'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~> 4.2'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
-gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -201,7 +202,7 @@ gem 'babosa', '~> 1.0.2'
gem 'loofah', '~> 2.0.3'
# Working with license
-gem 'licensee', '~> 8.0.0'
+gem 'licensee', '~> 8.7.0'
# Protect against bruteforcing
gem 'rack-attack', '~> 4.4.1'
@@ -229,19 +230,18 @@ gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
-gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.3.0'
-gem 'font-awesome-rails', '~> 4.6.1'
-gem 'gemojione', '~> 3.0'
-gem 'gon', '~> 6.1.0'
+gem 'addressable', '~> 2.3.8'
+gem 'bootstrap-sass', '~> 3.3.0'
+gem 'font-awesome-rails', '~> 4.7'
+gem 'gemojione', '~> 3.0'
+gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
-gem 'jquery-rails', '~> 4.1.0'
-gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'request_store', '~> 1.3'
-gem 'select2-rails', '~> 3.5.9'
-gem 'virtus', '~> 1.0.1'
-gem 'net-ssh', '~> 3.0.1'
-gem 'base32', '~> 0.3.0'
+gem 'jquery-rails', '~> 4.1.0'
+gem 'request_store', '~> 1.3'
+gem 'select2-rails', '~> 3.5.9'
+gem 'virtus', '~> 1.0.1'
+gem 'net-ssh', '~> 3.0.1'
+gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.0.0'
@@ -279,13 +279,13 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
- gem 'database_cleaner', '~> 1.5.0'
+ gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0'
- gem 'rspec-rails', '~> 3.5.0'
- gem 'rspec-retry', '~> 0.4.5'
- gem 'spinach-rails', '~> 0.2.1'
+ gem 'rspec-rails', '~> 3.5.0'
+ gem 'rspec-retry', '~> 0.4.5'
+ gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
- gem 'rspec_profiling', '~> 0.0.5'
+ gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -293,18 +293,18 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.6.2'
+ gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.9.0'
+ gem 'poltergeist', '~> 1.9.0'
- gem 'spring', '~> 1.7.0'
- gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'spring-commands-spinach', '~> 1.1.0'
+ gem 'spring', '~> 1.7.0'
+ gem 'spring-commands-rspec', '~> 1.0.4'
+ gem 'spring-commands-spinach', '~> 1.1.0'
- gem 'rubocop', '~> 0.46.0', require: false
- gem 'rubocop-rspec', '~> 1.9.1', require: false
+ gem 'rubocop', '~> 0.47.1', require: false
+ gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
- gem 'haml_lint', '~> 0.18.2', require: false
+ gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '0.12.0', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -329,8 +329,6 @@ group :test do
gem 'timecop', '~> 0.8.0'
end
-gem 'newrelic_rpm', '~> 3.16'
-
gem 'octokit', '~> 4.6.2'
gem 'mail_room', '~> 0.9.1'
@@ -347,8 +345,11 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.2'
# Health check
-gem 'health_check', '~> 2.2.0'
+gem 'health_check', '~> 2.6.0'
# System information
gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
+
+# Gitaly GRPC client
+gem 'gitaly', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2a3be763753..043ca4f8800 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
- ace-rails-ap (4.1.0)
+ ace-rails-ap (4.1.2)
actionmailer (4.2.8)
actionpack (= 4.2.8)
actionview (= 4.2.8)
@@ -78,6 +78,7 @@ GEM
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
@@ -103,11 +104,12 @@ GEM
capybara-screenshot (1.0.11)
capybara (>= 1.0, < 3)
launchy
- carrierwave (0.10.0)
+ carrierwave (0.11.2)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
mime-types (>= 1.16)
+ mimemagic (>= 0.3.0)
cause (0.1)
charlock_holmes (0.7.3)
chronic (0.10.2)
@@ -166,6 +168,9 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
+ doorkeeper-openid_connect (1.1.2)
+ doorkeeper (~> 4.0)
+ json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_trimmer (0.1.6)
@@ -231,7 +236,7 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.6.1.0)
+ font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
thor (~> 0.19.1)
@@ -245,6 +250,9 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
+ gitaly (0.3.0)
+ google-protobuf (~> 3.1)
+ grpc (~> 1.0)
github-linguist (4.7.6)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
@@ -296,6 +304,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
+ google-protobuf (3.2.0.2)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
@@ -304,7 +313,7 @@ GEM
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
- grape (0.18.0)
+ grape (0.19.1)
activesupport
builder
hashie (>= 2.1.0)
@@ -317,19 +326,22 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
+ grpc (1.1.2)
+ google-protobuf (~> 3.1)
+ googleauth (~> 0.5.1)
haml (4.0.7)
tilt
- haml_lint (0.18.2)
+ haml_lint (0.21.0)
haml (~> 4.0)
- rake (>= 10, < 12)
- rubocop (>= 0.36.0)
+ rake (>= 10, < 13)
+ rubocop (>= 0.47.0)
sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
thor
tilt
- hashie (3.4.4)
- health_check (2.2.1)
+ hashie (3.5.5)
+ health_check (2.6.0)
rails (>= 4.0)
hipchat (1.5.2)
httparty
@@ -353,8 +365,8 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.2)
- i18n (0.8.0)
- ice_nine (0.11.1)
+ i18n (0.8.1)
+ ice_nine (0.11.2)
influxdb (0.2.3)
cause
json
@@ -367,9 +379,13 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- jquery-ui-rails (5.0.5)
- railties (>= 3.2.16)
json (1.8.6)
+ json-jwt (1.7.1)
+ activesupport
+ bindata
+ multi_json (>= 1.3)
+ securecompare
+ url_safe_base64
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
@@ -398,8 +414,8 @@ GEM
rubyzip
thor
xml-simple
- licensee (8.0.0)
- rugged (>= 0.24b)
+ licensee (8.7.0)
+ rugged (~> 0.24)
little-plugger (1.1.4)
logging (2.1.0)
little-plugger (~> 1.1)
@@ -417,7 +433,7 @@ GEM
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.12.1)
- multi_xml (0.5.5)
+ multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (0.4.0)
tool (~> 0.2)
@@ -427,7 +443,6 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
- newrelic_rpm (3.16.0.318)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
@@ -441,7 +456,7 @@ GEM
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
- omniauth (1.3.2)
+ omniauth (1.4.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
@@ -501,7 +516,7 @@ GEM
os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
- parser (2.3.1.4)
+ parser (2.4.0.0)
ast (~> 2.2)
pg (0.18.4)
poltergeist (1.9.0)
@@ -579,7 +594,7 @@ GEM
recaptcha (3.0.0)
json
recursive-open-struct (1.0.0)
- redcarpet (3.3.3)
+ redcarpet (3.4.0)
redis (3.2.2)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
@@ -642,13 +657,13 @@ GEM
pg
rails
sqlite3
- rubocop (0.46.0)
- parser (>= 2.3.1.1, < 3.0)
+ rubocop (0.47.1)
+ parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- rubocop-rspec (1.9.1)
+ rubocop-rspec (1.12.0)
rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
@@ -660,7 +675,7 @@ GEM
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
- rubyzip (1.2.0)
+ rubyzip (1.2.1)
rufus-scheduler (3.1.10)
rugged (0.24.0)
safe_yaml (1.0.4)
@@ -679,6 +694,7 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
+ securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
@@ -758,8 +774,8 @@ GEM
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (0.19.4)
- thread_safe (0.3.5)
- tilt (2.0.5)
+ thread_safe (0.3.6)
+ tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tool (0.2.3)
@@ -776,7 +792,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.1.1)
+ unicode-display_width (1.1.3)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -784,6 +800,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
@@ -846,7 +863,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 0.10.0)
+ carrierwave (~> 0.11.0)
charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -861,6 +878,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
+ doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
@@ -873,11 +891,12 @@ DEPENDENCIES
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
- font-awesome-rails (~> 4.6.1)
+ font-awesome-rails (~> 4.7)
foreman (~> 0.78.0)
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
+ gitaly (~> 0.3.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
@@ -886,11 +905,11 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
- grape (~> 0.18.0)
+ grape (~> 0.19.0)
grape-entity (~> 0.6.0)
- haml_lint (~> 0.18.2)
+ haml_lint (~> 0.21.0)
hamlit (~> 2.6.1)
- health_check (~> 2.2.0)
+ health_check (~> 2.6.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
html2text
@@ -899,7 +918,6 @@ DEPENDENCIES
jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
- jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2)
jwt (~> 1.5.6)
kaminari (~> 0.17.0)
@@ -907,7 +925,7 @@ DEPENDENCIES
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0)
- licensee (~> 8.0.0)
+ licensee (~> 8.7.0)
loofah (~> 2.0.3)
mail_room (~> 0.9.1)
method_source (~> 0.8)
@@ -915,12 +933,11 @@ DEPENDENCIES
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1)
- newrelic_rpm (~> 3.16)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
- omniauth (~> 1.3.2)
+ omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6)
@@ -952,7 +969,7 @@ DEPENDENCIES
rblineprof (~> 0.3.6)
rdoc (~> 4.2)
recaptcha (~> 3.0)
- redcarpet (~> 3.3.3)
+ redcarpet (~> 3.4)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1)
@@ -963,8 +980,8 @@ DEPENDENCIES
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.46.0)
- rubocop-rspec (~> 1.9.1)
+ rubocop (~> 0.47.1)
+ rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
@@ -1011,4 +1028,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.14.3
+ 1.14.5
diff --git a/VERSION b/VERSION
index 5c99c061a47..64de8316674 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.17.0-pre
+8.18.0-pre
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 6f1a34a5591..5dcd9c09b70 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png
new file mode 100644
index 00000000000..6903ff0304a
--- /dev/null
+++ b/app/assets/images/emoji/100.png
Binary files differ
diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png
new file mode 100644
index 00000000000..248dc7e55b6
--- /dev/null
+++ b/app/assets/images/emoji/1234.png
Binary files differ
diff --git a/app/assets/images/emoji/1F627.png b/app/assets/images/emoji/1F627.png
new file mode 100644
index 00000000000..f99026a3bc7
--- /dev/null
+++ b/app/assets/images/emoji/1F627.png
Binary files differ
diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png
new file mode 100644
index 00000000000..38ca662eded
--- /dev/null
+++ b/app/assets/images/emoji/8ball.png
Binary files differ
diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png
new file mode 100644
index 00000000000..8603ff05a17
--- /dev/null
+++ b/app/assets/images/emoji/a.png
Binary files differ
diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png
new file mode 100644
index 00000000000..d9f2d17dea0
--- /dev/null
+++ b/app/assets/images/emoji/ab.png
Binary files differ
diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png
new file mode 100644
index 00000000000..7688de692a9
--- /dev/null
+++ b/app/assets/images/emoji/abc.png
Binary files differ
diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png
new file mode 100644
index 00000000000..0996a870570
--- /dev/null
+++ b/app/assets/images/emoji/abcd.png
Binary files differ
diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png
new file mode 100644
index 00000000000..8afd7ce99cf
--- /dev/null
+++ b/app/assets/images/emoji/accept.png
Binary files differ
diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png
new file mode 100644
index 00000000000..3eb4b61bf1d
--- /dev/null
+++ b/app/assets/images/emoji/aerial_tramway.png
Binary files differ
diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png
new file mode 100644
index 00000000000..268d2ac3c8e
--- /dev/null
+++ b/app/assets/images/emoji/airplane.png
Binary files differ
diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png
new file mode 100644
index 00000000000..d66841962f2
--- /dev/null
+++ b/app/assets/images/emoji/airplane_arriving.png
Binary files differ
diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png
new file mode 100644
index 00000000000..a5766f9f4ae
--- /dev/null
+++ b/app/assets/images/emoji/airplane_departure.png
Binary files differ
diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png
new file mode 100644
index 00000000000..b731b15e3a8
--- /dev/null
+++ b/app/assets/images/emoji/airplane_small.png
Binary files differ
diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png
new file mode 100644
index 00000000000..cdbc2fbb950
--- /dev/null
+++ b/app/assets/images/emoji/alarm_clock.png
Binary files differ
diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png
new file mode 100644
index 00000000000..307a7324249
--- /dev/null
+++ b/app/assets/images/emoji/alembic.png
Binary files differ
diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png
new file mode 100644
index 00000000000..3b90e97433b
--- /dev/null
+++ b/app/assets/images/emoji/alien.png
Binary files differ
diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png
new file mode 100644
index 00000000000..6fb8076d766
--- /dev/null
+++ b/app/assets/images/emoji/ambulance.png
Binary files differ
diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png
new file mode 100644
index 00000000000..96de5056059
--- /dev/null
+++ b/app/assets/images/emoji/amphora.png
Binary files differ
diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png
new file mode 100644
index 00000000000..b036f70a00b
--- /dev/null
+++ b/app/assets/images/emoji/anchor.png
Binary files differ
diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png
new file mode 100644
index 00000000000..66ea97a3b99
--- /dev/null
+++ b/app/assets/images/emoji/angel.png
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png
new file mode 100644
index 00000000000..391694dc07e
--- /dev/null
+++ b/app/assets/images/emoji/angel_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png
new file mode 100644
index 00000000000..700cbe6ed2c
--- /dev/null
+++ b/app/assets/images/emoji/angel_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png
new file mode 100644
index 00000000000..be597437d25
--- /dev/null
+++ b/app/assets/images/emoji/angel_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png
new file mode 100644
index 00000000000..b06d3c853ef
--- /dev/null
+++ b/app/assets/images/emoji/angel_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png
new file mode 100644
index 00000000000..17bd677e334
--- /dev/null
+++ b/app/assets/images/emoji/angel_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png
new file mode 100644
index 00000000000..d63c2e000e4
--- /dev/null
+++ b/app/assets/images/emoji/anger.png
Binary files differ
diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png
new file mode 100644
index 00000000000..f5c97c4d297
--- /dev/null
+++ b/app/assets/images/emoji/anger_right.png
Binary files differ
diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png
new file mode 100644
index 00000000000..cfc4a6ecde5
--- /dev/null
+++ b/app/assets/images/emoji/angry.png
Binary files differ
diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png
new file mode 100644
index 00000000000..994127ed6b3
--- /dev/null
+++ b/app/assets/images/emoji/ant.png
Binary files differ
diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png
new file mode 100644
index 00000000000..da650c60f62
--- /dev/null
+++ b/app/assets/images/emoji/apple.png
Binary files differ
diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png
new file mode 100644
index 00000000000..641a4f68889
--- /dev/null
+++ b/app/assets/images/emoji/aquarius.png
Binary files differ
diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png
new file mode 100644
index 00000000000..21a189d0ede
--- /dev/null
+++ b/app/assets/images/emoji/aries.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png
new file mode 100644
index 00000000000..ee38e3b038e
--- /dev/null
+++ b/app/assets/images/emoji/arrow_backward.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png
new file mode 100644
index 00000000000..90193bfcb40
--- /dev/null
+++ b/app/assets/images/emoji/arrow_double_down.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png
new file mode 100644
index 00000000000..13543d5eef2
--- /dev/null
+++ b/app/assets/images/emoji/arrow_double_up.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png
new file mode 100644
index 00000000000..b8eefd0b19f
--- /dev/null
+++ b/app/assets/images/emoji/arrow_down.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png
new file mode 100644
index 00000000000..5870b9a2241
--- /dev/null
+++ b/app/assets/images/emoji/arrow_down_small.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png
new file mode 100644
index 00000000000..4e2b682857c
--- /dev/null
+++ b/app/assets/images/emoji/arrow_forward.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png
new file mode 100644
index 00000000000..2d9d24bca80
--- /dev/null
+++ b/app/assets/images/emoji/arrow_heading_down.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png
new file mode 100644
index 00000000000..f29bfcfc0de
--- /dev/null
+++ b/app/assets/images/emoji/arrow_heading_up.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png
new file mode 100644
index 00000000000..8c685e0a81b
--- /dev/null
+++ b/app/assets/images/emoji/arrow_left.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png
new file mode 100644
index 00000000000..88b37716078
--- /dev/null
+++ b/app/assets/images/emoji/arrow_lower_left.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png
new file mode 100644
index 00000000000..7e807da7392
--- /dev/null
+++ b/app/assets/images/emoji/arrow_lower_right.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png
new file mode 100644
index 00000000000..4755670b5cc
--- /dev/null
+++ b/app/assets/images/emoji/arrow_right.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png
new file mode 100644
index 00000000000..e7258ad3268
--- /dev/null
+++ b/app/assets/images/emoji/arrow_right_hook.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png
new file mode 100644
index 00000000000..af8218a87f7
--- /dev/null
+++ b/app/assets/images/emoji/arrow_up.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png
new file mode 100644
index 00000000000..dfa32b97186
--- /dev/null
+++ b/app/assets/images/emoji/arrow_up_down.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png
new file mode 100644
index 00000000000..20a13dcd5cd
--- /dev/null
+++ b/app/assets/images/emoji/arrow_up_small.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png
new file mode 100644
index 00000000000..f38718fbe34
--- /dev/null
+++ b/app/assets/images/emoji/arrow_upper_left.png
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png
new file mode 100644
index 00000000000..c43e12d0f64
--- /dev/null
+++ b/app/assets/images/emoji/arrow_upper_right.png
Binary files differ
diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png
new file mode 100644
index 00000000000..26e49c38388
--- /dev/null
+++ b/app/assets/images/emoji/arrows_clockwise.png
Binary files differ
diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png
new file mode 100644
index 00000000000..8d06d8e0912
--- /dev/null
+++ b/app/assets/images/emoji/arrows_counterclockwise.png
Binary files differ
diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png
new file mode 100644
index 00000000000..bd6afe9ff06
--- /dev/null
+++ b/app/assets/images/emoji/art.png
Binary files differ
diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png
new file mode 100644
index 00000000000..c8217317132
--- /dev/null
+++ b/app/assets/images/emoji/articulated_lorry.png
Binary files differ
diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png
new file mode 100644
index 00000000000..2f8e5113803
--- /dev/null
+++ b/app/assets/images/emoji/asterisk.png
Binary files differ
diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png
new file mode 100644
index 00000000000..bd0ac55ec8e
--- /dev/null
+++ b/app/assets/images/emoji/astonished.png
Binary files differ
diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png
new file mode 100644
index 00000000000..423fa07dd5d
--- /dev/null
+++ b/app/assets/images/emoji/athletic_shoe.png
Binary files differ
diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png
new file mode 100644
index 00000000000..4d935307b94
--- /dev/null
+++ b/app/assets/images/emoji/atm.png
Binary files differ
diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png
new file mode 100644
index 00000000000..5f4567aa093
--- /dev/null
+++ b/app/assets/images/emoji/atom.png
Binary files differ
diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png
new file mode 100644
index 00000000000..06f0d124aed
--- /dev/null
+++ b/app/assets/images/emoji/avocado.png
Binary files differ
diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png
new file mode 100644
index 00000000000..25875bc6a14
--- /dev/null
+++ b/app/assets/images/emoji/b.png
Binary files differ
diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png
new file mode 100644
index 00000000000..a4af92c63c7
--- /dev/null
+++ b/app/assets/images/emoji/baby.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png
new file mode 100644
index 00000000000..2bd10524180
--- /dev/null
+++ b/app/assets/images/emoji/baby_bottle.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png
new file mode 100644
index 00000000000..dccd96576ea
--- /dev/null
+++ b/app/assets/images/emoji/baby_chick.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png
new file mode 100644
index 00000000000..64a10b71710
--- /dev/null
+++ b/app/assets/images/emoji/baby_symbol.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png
new file mode 100644
index 00000000000..d20911d40db
--- /dev/null
+++ b/app/assets/images/emoji/baby_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png
new file mode 100644
index 00000000000..b0a9b30ed17
--- /dev/null
+++ b/app/assets/images/emoji/baby_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png
new file mode 100644
index 00000000000..7de5286fac1
--- /dev/null
+++ b/app/assets/images/emoji/baby_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png
new file mode 100644
index 00000000000..9b7a86ac615
--- /dev/null
+++ b/app/assets/images/emoji/baby_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png
new file mode 100644
index 00000000000..fe1be34cb88
--- /dev/null
+++ b/app/assets/images/emoji/baby_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png
new file mode 100644
index 00000000000..d32c5d4f17f
--- /dev/null
+++ b/app/assets/images/emoji/back.png
Binary files differ
diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png
new file mode 100644
index 00000000000..f38a485fbe4
--- /dev/null
+++ b/app/assets/images/emoji/bacon.png
Binary files differ
diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png
new file mode 100644
index 00000000000..7ba15708990
--- /dev/null
+++ b/app/assets/images/emoji/badminton.png
Binary files differ
diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png
new file mode 100644
index 00000000000..409b593e78a
--- /dev/null
+++ b/app/assets/images/emoji/baggage_claim.png
Binary files differ
diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png
new file mode 100644
index 00000000000..07916fe6df1
--- /dev/null
+++ b/app/assets/images/emoji/balloon.png
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png
new file mode 100644
index 00000000000..9b6767aea9e
--- /dev/null
+++ b/app/assets/images/emoji/ballot_box.png
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png
new file mode 100644
index 00000000000..284d9573847
--- /dev/null
+++ b/app/assets/images/emoji/ballot_box_with_check.png
Binary files differ
diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png
new file mode 100644
index 00000000000..5d5e0e728a0
--- /dev/null
+++ b/app/assets/images/emoji/bamboo.png
Binary files differ
diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png
new file mode 100644
index 00000000000..f4987279580
--- /dev/null
+++ b/app/assets/images/emoji/banana.png
Binary files differ
diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png
new file mode 100644
index 00000000000..58a9c528fca
--- /dev/null
+++ b/app/assets/images/emoji/bangbang.png
Binary files differ
diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png
new file mode 100644
index 00000000000..dffdcef36a1
--- /dev/null
+++ b/app/assets/images/emoji/bank.png
Binary files differ
diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png
new file mode 100644
index 00000000000..53c89455008
--- /dev/null
+++ b/app/assets/images/emoji/bar_chart.png
Binary files differ
diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png
new file mode 100644
index 00000000000..896f4d716cf
--- /dev/null
+++ b/app/assets/images/emoji/barber.png
Binary files differ
diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png
new file mode 100644
index 00000000000..f8463f1538b
--- /dev/null
+++ b/app/assets/images/emoji/baseball.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png
new file mode 100644
index 00000000000..64c76b79c6d
--- /dev/null
+++ b/app/assets/images/emoji/basketball.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png
new file mode 100644
index 00000000000..8ce90c5cad6
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png
new file mode 100644
index 00000000000..cd12c7ab9bf
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png
new file mode 100644
index 00000000000..f892fd596da
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png
new file mode 100644
index 00000000000..e109997a91a
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png
new file mode 100644
index 00000000000..3b90b946af4
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png
new file mode 100644
index 00000000000..bafed7828a7
--- /dev/null
+++ b/app/assets/images/emoji/basketball_player_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png
new file mode 100644
index 00000000000..3152c047e00
--- /dev/null
+++ b/app/assets/images/emoji/bat.png
Binary files differ
diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png
new file mode 100644
index 00000000000..43fba5c8a28
--- /dev/null
+++ b/app/assets/images/emoji/bath.png
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png
new file mode 100644
index 00000000000..2152eabf2f5
--- /dev/null
+++ b/app/assets/images/emoji/bath_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png
new file mode 100644
index 00000000000..2102e6133e3
--- /dev/null
+++ b/app/assets/images/emoji/bath_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png
new file mode 100644
index 00000000000..fae66181e9f
--- /dev/null
+++ b/app/assets/images/emoji/bath_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png
new file mode 100644
index 00000000000..1f8959d0d99
--- /dev/null
+++ b/app/assets/images/emoji/bath_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png
new file mode 100644
index 00000000000..c8a08e84f25
--- /dev/null
+++ b/app/assets/images/emoji/bath_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png
new file mode 100644
index 00000000000..9a5f09361eb
--- /dev/null
+++ b/app/assets/images/emoji/bathtub.png
Binary files differ
diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png
new file mode 100644
index 00000000000..f593e2bdb65
--- /dev/null
+++ b/app/assets/images/emoji/battery.png
Binary files differ
diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png
new file mode 100644
index 00000000000..69108c8ea10
--- /dev/null
+++ b/app/assets/images/emoji/beach.png
Binary files differ
diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png
new file mode 100644
index 00000000000..220a74f8132
--- /dev/null
+++ b/app/assets/images/emoji/beach_umbrella.png
Binary files differ
diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png
new file mode 100644
index 00000000000..272d56bbbcc
--- /dev/null
+++ b/app/assets/images/emoji/bear.png
Binary files differ
diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png
new file mode 100644
index 00000000000..86f964e245d
--- /dev/null
+++ b/app/assets/images/emoji/bed.png
Binary files differ
diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png
new file mode 100644
index 00000000000..46156060096
--- /dev/null
+++ b/app/assets/images/emoji/bee.png
Binary files differ
diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png
new file mode 100644
index 00000000000..b6d73dc0b7a
--- /dev/null
+++ b/app/assets/images/emoji/beer.png
Binary files differ
diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png
new file mode 100644
index 00000000000..b55deb66b41
--- /dev/null
+++ b/app/assets/images/emoji/beers.png
Binary files differ
diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png
new file mode 100644
index 00000000000..3d93174d7fc
--- /dev/null
+++ b/app/assets/images/emoji/beetle.png
Binary files differ
diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png
new file mode 100644
index 00000000000..bc434fb7cb5
--- /dev/null
+++ b/app/assets/images/emoji/beginner.png
Binary files differ
diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png
new file mode 100644
index 00000000000..5b3b0461999
--- /dev/null
+++ b/app/assets/images/emoji/bell.png
Binary files differ
diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png
new file mode 100644
index 00000000000..6b3297ceaf7
--- /dev/null
+++ b/app/assets/images/emoji/bellhop.png
Binary files differ
diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png
new file mode 100644
index 00000000000..83d41ca7eb9
--- /dev/null
+++ b/app/assets/images/emoji/bento.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png
new file mode 100644
index 00000000000..9274da11048
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png
new file mode 100644
index 00000000000..decc2f728fe
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png
new file mode 100644
index 00000000000..0067717b80a
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png
new file mode 100644
index 00000000000..a4f7b5e2776
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png
new file mode 100644
index 00000000000..a3c8a797db4
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png
new file mode 100644
index 00000000000..1606a874051
--- /dev/null
+++ b/app/assets/images/emoji/bicyclist_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png
new file mode 100644
index 00000000000..556ed70f1a7
--- /dev/null
+++ b/app/assets/images/emoji/bike.png
Binary files differ
diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png
new file mode 100644
index 00000000000..77a8a0aae5b
--- /dev/null
+++ b/app/assets/images/emoji/bikini.png
Binary files differ
diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png
new file mode 100644
index 00000000000..007b4fc2d85
--- /dev/null
+++ b/app/assets/images/emoji/biohazard.png
Binary files differ
diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png
new file mode 100644
index 00000000000..e201c22be33
--- /dev/null
+++ b/app/assets/images/emoji/bird.png
Binary files differ
diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png
new file mode 100644
index 00000000000..317e9a41949
--- /dev/null
+++ b/app/assets/images/emoji/birthday.png
Binary files differ
diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png
new file mode 100644
index 00000000000..b62b87170e8
--- /dev/null
+++ b/app/assets/images/emoji/black_circle.png
Binary files differ
diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png
new file mode 100644
index 00000000000..b4068c3e6e8
--- /dev/null
+++ b/app/assets/images/emoji/black_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png
new file mode 100644
index 00000000000..3d0924b68aa
--- /dev/null
+++ b/app/assets/images/emoji/black_joker.png
Binary files differ
diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png
new file mode 100644
index 00000000000..162f2bb4290
--- /dev/null
+++ b/app/assets/images/emoji/black_large_square.png
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png
new file mode 100644
index 00000000000..39765bba610
--- /dev/null
+++ b/app/assets/images/emoji/black_medium_small_square.png
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png
new file mode 100644
index 00000000000..05a30a6aa2d
--- /dev/null
+++ b/app/assets/images/emoji/black_medium_square.png
Binary files differ
diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png
new file mode 100644
index 00000000000..872d0ae1598
--- /dev/null
+++ b/app/assets/images/emoji/black_nib.png
Binary files differ
diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png
new file mode 100644
index 00000000000..48595d3e1a9
--- /dev/null
+++ b/app/assets/images/emoji/black_small_square.png
Binary files differ
diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png
new file mode 100644
index 00000000000..a78fc2f6b63
--- /dev/null
+++ b/app/assets/images/emoji/black_square_button.png
Binary files differ
diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png
new file mode 100644
index 00000000000..4083026c157
--- /dev/null
+++ b/app/assets/images/emoji/blossom.png
Binary files differ
diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png
new file mode 100644
index 00000000000..a10f4f84e35
--- /dev/null
+++ b/app/assets/images/emoji/blowfish.png
Binary files differ
diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png
new file mode 100644
index 00000000000..e1e455401cc
--- /dev/null
+++ b/app/assets/images/emoji/blue_book.png
Binary files differ
diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png
new file mode 100644
index 00000000000..e8ba817d393
--- /dev/null
+++ b/app/assets/images/emoji/blue_car.png
Binary files differ
diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png
new file mode 100644
index 00000000000..bdf1287e55e
--- /dev/null
+++ b/app/assets/images/emoji/blue_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png
new file mode 100644
index 00000000000..aac1a424ad4
--- /dev/null
+++ b/app/assets/images/emoji/blush.png
Binary files differ
diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png
new file mode 100644
index 00000000000..fead972633c
--- /dev/null
+++ b/app/assets/images/emoji/boar.png
Binary files differ
diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png
new file mode 100644
index 00000000000..c7f8f81c939
--- /dev/null
+++ b/app/assets/images/emoji/bomb.png
Binary files differ
diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png
new file mode 100644
index 00000000000..0f4447ed396
--- /dev/null
+++ b/app/assets/images/emoji/book.png
Binary files differ
diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png
new file mode 100644
index 00000000000..bbb444611f0
--- /dev/null
+++ b/app/assets/images/emoji/bookmark.png
Binary files differ
diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png
new file mode 100644
index 00000000000..f8d9e01b428
--- /dev/null
+++ b/app/assets/images/emoji/bookmark_tabs.png
Binary files differ
diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png
new file mode 100644
index 00000000000..59a8bafeb0d
--- /dev/null
+++ b/app/assets/images/emoji/books.png
Binary files differ
diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png
new file mode 100644
index 00000000000..9b0f027b1a8
--- /dev/null
+++ b/app/assets/images/emoji/boom.png
Binary files differ
diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png
new file mode 100644
index 00000000000..11f1065ed07
--- /dev/null
+++ b/app/assets/images/emoji/boot.png
Binary files differ
diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png
new file mode 100644
index 00000000000..11455af6df4
--- /dev/null
+++ b/app/assets/images/emoji/bouquet.png
Binary files differ
diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png
new file mode 100644
index 00000000000..d8f793088dc
--- /dev/null
+++ b/app/assets/images/emoji/bow.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png
new file mode 100644
index 00000000000..6a538bf475f
--- /dev/null
+++ b/app/assets/images/emoji/bow_and_arrow.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png
new file mode 100644
index 00000000000..87afb7b54cf
--- /dev/null
+++ b/app/assets/images/emoji/bow_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png
new file mode 100644
index 00000000000..3ccf7dc0850
--- /dev/null
+++ b/app/assets/images/emoji/bow_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png
new file mode 100644
index 00000000000..8b9eb64f926
--- /dev/null
+++ b/app/assets/images/emoji/bow_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png
new file mode 100644
index 00000000000..683795ff40d
--- /dev/null
+++ b/app/assets/images/emoji/bow_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png
new file mode 100644
index 00000000000..7969d971752
--- /dev/null
+++ b/app/assets/images/emoji/bow_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png
new file mode 100644
index 00000000000..63add89e53b
--- /dev/null
+++ b/app/assets/images/emoji/bowling.png
Binary files differ
diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png
new file mode 100644
index 00000000000..9838f24e51a
--- /dev/null
+++ b/app/assets/images/emoji/boxing_glove.png
Binary files differ
diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png
new file mode 100644
index 00000000000..8ecfb0a4e92
--- /dev/null
+++ b/app/assets/images/emoji/boy.png
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png
new file mode 100644
index 00000000000..2fc436ea512
--- /dev/null
+++ b/app/assets/images/emoji/boy_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png
new file mode 100644
index 00000000000..09a5f18d360
--- /dev/null
+++ b/app/assets/images/emoji/boy_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png
new file mode 100644
index 00000000000..3cfe675dd3a
--- /dev/null
+++ b/app/assets/images/emoji/boy_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png
new file mode 100644
index 00000000000..780be0ace36
--- /dev/null
+++ b/app/assets/images/emoji/boy_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png
new file mode 100644
index 00000000000..f32fe22e35c
--- /dev/null
+++ b/app/assets/images/emoji/boy_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png
new file mode 100644
index 00000000000..6676510aaa5
--- /dev/null
+++ b/app/assets/images/emoji/bread.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png
new file mode 100644
index 00000000000..eaf4bd97890
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png
new file mode 100644
index 00000000000..c4fb141ae8f
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png
new file mode 100644
index 00000000000..c248769fc06
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png
new file mode 100644
index 00000000000..962c0a6eedb
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png
new file mode 100644
index 00000000000..740ca208cd4
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png
new file mode 100644
index 00000000000..5cc5598587d
--- /dev/null
+++ b/app/assets/images/emoji/bride_with_veil_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png
new file mode 100644
index 00000000000..1d444e0be65
--- /dev/null
+++ b/app/assets/images/emoji/bridge_at_night.png
Binary files differ
diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png
new file mode 100644
index 00000000000..b9912ba2148
--- /dev/null
+++ b/app/assets/images/emoji/briefcase.png
Binary files differ
diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png
new file mode 100644
index 00000000000..718e26ee122
--- /dev/null
+++ b/app/assets/images/emoji/broken_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png
new file mode 100644
index 00000000000..e64e72f259a
--- /dev/null
+++ b/app/assets/images/emoji/bug.png
Binary files differ
diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png
new file mode 100644
index 00000000000..38e32e02d9f
--- /dev/null
+++ b/app/assets/images/emoji/bulb.png
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png
new file mode 100644
index 00000000000..4f698e056fa
--- /dev/null
+++ b/app/assets/images/emoji/bullettrain_front.png
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png
new file mode 100644
index 00000000000..ed61c67bf07
--- /dev/null
+++ b/app/assets/images/emoji/bullettrain_side.png
Binary files differ
diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png
new file mode 100644
index 00000000000..02bd5601df7
--- /dev/null
+++ b/app/assets/images/emoji/burrito.png
Binary files differ
diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png
new file mode 100644
index 00000000000..641ddc56ca7
--- /dev/null
+++ b/app/assets/images/emoji/bus.png
Binary files differ
diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png
new file mode 100644
index 00000000000..b2b62208bfd
--- /dev/null
+++ b/app/assets/images/emoji/busstop.png
Binary files differ
diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png
new file mode 100644
index 00000000000..123b2cbe1fb
--- /dev/null
+++ b/app/assets/images/emoji/bust_in_silhouette.png
Binary files differ
diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png
new file mode 100644
index 00000000000..d7656860a1c
--- /dev/null
+++ b/app/assets/images/emoji/busts_in_silhouette.png
Binary files differ
diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png
new file mode 100644
index 00000000000..5631fe99226
--- /dev/null
+++ b/app/assets/images/emoji/butterfly.png
Binary files differ
diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png
new file mode 100644
index 00000000000..9b48ccf3d0c
--- /dev/null
+++ b/app/assets/images/emoji/cactus.png
Binary files differ
diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png
new file mode 100644
index 00000000000..4368177be9a
--- /dev/null
+++ b/app/assets/images/emoji/cake.png
Binary files differ
diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png
new file mode 100644
index 00000000000..47353b74447
--- /dev/null
+++ b/app/assets/images/emoji/calendar.png
Binary files differ
diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png
new file mode 100644
index 00000000000..dec8d49bfa8
--- /dev/null
+++ b/app/assets/images/emoji/calendar_spiral.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png
new file mode 100644
index 00000000000..a10c59ba711
--- /dev/null
+++ b/app/assets/images/emoji/call_me.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png
new file mode 100644
index 00000000000..2c93201181a
--- /dev/null
+++ b/app/assets/images/emoji/call_me_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png
new file mode 100644
index 00000000000..c39f45a41ed
--- /dev/null
+++ b/app/assets/images/emoji/call_me_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png
new file mode 100644
index 00000000000..83a57f63c29
--- /dev/null
+++ b/app/assets/images/emoji/call_me_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png
new file mode 100644
index 00000000000..65b3468fe44
--- /dev/null
+++ b/app/assets/images/emoji/call_me_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png
new file mode 100644
index 00000000000..94ef68ff3b3
--- /dev/null
+++ b/app/assets/images/emoji/call_me_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png
new file mode 100644
index 00000000000..e2f308f8e46
--- /dev/null
+++ b/app/assets/images/emoji/calling.png
Binary files differ
diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png
new file mode 100644
index 00000000000..b421d07a805
--- /dev/null
+++ b/app/assets/images/emoji/camel.png
Binary files differ
diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png
new file mode 100644
index 00000000000..0a3429f72ef
--- /dev/null
+++ b/app/assets/images/emoji/camera.png
Binary files differ
diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png
new file mode 100644
index 00000000000..27471da2029
--- /dev/null
+++ b/app/assets/images/emoji/camera_with_flash.png
Binary files differ
diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png
new file mode 100644
index 00000000000..d589cc1f44b
--- /dev/null
+++ b/app/assets/images/emoji/camping.png
Binary files differ
diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png
new file mode 100644
index 00000000000..a64af07cb5f
--- /dev/null
+++ b/app/assets/images/emoji/cancer.png
Binary files differ
diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png
new file mode 100644
index 00000000000..0b56444e355
--- /dev/null
+++ b/app/assets/images/emoji/candle.png
Binary files differ
diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png
new file mode 100644
index 00000000000..8c67ace3a35
--- /dev/null
+++ b/app/assets/images/emoji/candy.png
Binary files differ
diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png
new file mode 100644
index 00000000000..e26cdb9da69
--- /dev/null
+++ b/app/assets/images/emoji/canoe.png
Binary files differ
diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png
new file mode 100644
index 00000000000..fe9482d2d8a
--- /dev/null
+++ b/app/assets/images/emoji/capital_abcd.png
Binary files differ
diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png
new file mode 100644
index 00000000000..6293d31d4b1
--- /dev/null
+++ b/app/assets/images/emoji/capricorn.png
Binary files differ
diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png
new file mode 100644
index 00000000000..f2e764ce59d
--- /dev/null
+++ b/app/assets/images/emoji/card_box.png
Binary files differ
diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png
new file mode 100644
index 00000000000..151e11cb3b4
--- /dev/null
+++ b/app/assets/images/emoji/card_index.png
Binary files differ
diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png
new file mode 100644
index 00000000000..a17074edf05
--- /dev/null
+++ b/app/assets/images/emoji/carousel_horse.png
Binary files differ
diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png
new file mode 100644
index 00000000000..c68829b58e7
--- /dev/null
+++ b/app/assets/images/emoji/carrot.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png
new file mode 100644
index 00000000000..cbcaa578253
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png
new file mode 100644
index 00000000000..db6d65895fb
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png
new file mode 100644
index 00000000000..e00ffbc27a8
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png
new file mode 100644
index 00000000000..49321be391f
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png
new file mode 100644
index 00000000000..d4562b5e3dd
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png
new file mode 100644
index 00000000000..6e09a870767
--- /dev/null
+++ b/app/assets/images/emoji/cartwheel_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png
new file mode 100644
index 00000000000..efd82c2abf3
--- /dev/null
+++ b/app/assets/images/emoji/cat.png
Binary files differ
diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png
new file mode 100644
index 00000000000..46abe8cbc14
--- /dev/null
+++ b/app/assets/images/emoji/cat2.png
Binary files differ
diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png
new file mode 100644
index 00000000000..e6b01449cd9
--- /dev/null
+++ b/app/assets/images/emoji/cd.png
Binary files differ
diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png
new file mode 100644
index 00000000000..57f46139a06
--- /dev/null
+++ b/app/assets/images/emoji/chains.png
Binary files differ
diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png
new file mode 100644
index 00000000000..285a79a93d0
--- /dev/null
+++ b/app/assets/images/emoji/champagne.png
Binary files differ
diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png
new file mode 100644
index 00000000000..31937ae9392
--- /dev/null
+++ b/app/assets/images/emoji/champagne_glass.png
Binary files differ
diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png
new file mode 100644
index 00000000000..9773f03be22
--- /dev/null
+++ b/app/assets/images/emoji/chart.png
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png
new file mode 100644
index 00000000000..5222ec72d85
--- /dev/null
+++ b/app/assets/images/emoji/chart_with_downwards_trend.png
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png
new file mode 100644
index 00000000000..f13cfcf9956
--- /dev/null
+++ b/app/assets/images/emoji/chart_with_upwards_trend.png
Binary files differ
diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png
new file mode 100644
index 00000000000..5a71eecb89b
--- /dev/null
+++ b/app/assets/images/emoji/checkered_flag.png
Binary files differ
diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png
new file mode 100644
index 00000000000..00e99762286
--- /dev/null
+++ b/app/assets/images/emoji/cheese.png
Binary files differ
diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png
new file mode 100644
index 00000000000..9b10cbaac5e
--- /dev/null
+++ b/app/assets/images/emoji/cherries.png
Binary files differ
diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png
new file mode 100644
index 00000000000..282f3e7bc81
--- /dev/null
+++ b/app/assets/images/emoji/cherry_blossom.png
Binary files differ
diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png
new file mode 100644
index 00000000000..e9fb40468ed
--- /dev/null
+++ b/app/assets/images/emoji/chestnut.png
Binary files differ
diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png
new file mode 100644
index 00000000000..9a6992e55ba
--- /dev/null
+++ b/app/assets/images/emoji/chicken.png
Binary files differ
diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png
new file mode 100644
index 00000000000..fa4c091c7c3
--- /dev/null
+++ b/app/assets/images/emoji/children_crossing.png
Binary files differ
diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png
new file mode 100644
index 00000000000..2aac560cb22
--- /dev/null
+++ b/app/assets/images/emoji/chipmunk.png
Binary files differ
diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png
new file mode 100644
index 00000000000..318bbd40ef9
--- /dev/null
+++ b/app/assets/images/emoji/chocolate_bar.png
Binary files differ
diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png
new file mode 100644
index 00000000000..4197d37a52b
--- /dev/null
+++ b/app/assets/images/emoji/christmas_tree.png
Binary files differ
diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png
new file mode 100644
index 00000000000..8242fd272b3
--- /dev/null
+++ b/app/assets/images/emoji/church.png
Binary files differ
diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png
new file mode 100644
index 00000000000..65f27b386f2
--- /dev/null
+++ b/app/assets/images/emoji/cinema.png
Binary files differ
diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png
new file mode 100644
index 00000000000..b0379775b12
--- /dev/null
+++ b/app/assets/images/emoji/circus_tent.png
Binary files differ
diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png
new file mode 100644
index 00000000000..80cdff7cf5d
--- /dev/null
+++ b/app/assets/images/emoji/city_dusk.png
Binary files differ
diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png
new file mode 100644
index 00000000000..7cded0ba55b
--- /dev/null
+++ b/app/assets/images/emoji/city_sunset.png
Binary files differ
diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png
new file mode 100644
index 00000000000..d7b9844a0b4
--- /dev/null
+++ b/app/assets/images/emoji/cityscape.png
Binary files differ
diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png
new file mode 100644
index 00000000000..8b01b4343e2
--- /dev/null
+++ b/app/assets/images/emoji/cl.png
Binary files differ
diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png
new file mode 100644
index 00000000000..b0ffe928920
--- /dev/null
+++ b/app/assets/images/emoji/clap.png
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png
new file mode 100644
index 00000000000..de4bc837b96
--- /dev/null
+++ b/app/assets/images/emoji/clap_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png
new file mode 100644
index 00000000000..1323de775ba
--- /dev/null
+++ b/app/assets/images/emoji/clap_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png
new file mode 100644
index 00000000000..d448ca19dde
--- /dev/null
+++ b/app/assets/images/emoji/clap_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png
new file mode 100644
index 00000000000..c49f44ee91d
--- /dev/null
+++ b/app/assets/images/emoji/clap_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png
new file mode 100644
index 00000000000..29ee9bdf37c
--- /dev/null
+++ b/app/assets/images/emoji/clap_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png
new file mode 100644
index 00000000000..81390883111
--- /dev/null
+++ b/app/assets/images/emoji/clapper.png
Binary files differ
diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png
new file mode 100644
index 00000000000..de7b559daaf
--- /dev/null
+++ b/app/assets/images/emoji/classical_building.png
Binary files differ
diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png
new file mode 100644
index 00000000000..7edcfc52509
--- /dev/null
+++ b/app/assets/images/emoji/clipboard.png
Binary files differ
diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png
new file mode 100644
index 00000000000..ffdb451e3a8
--- /dev/null
+++ b/app/assets/images/emoji/clock.png
Binary files differ
diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png
new file mode 100644
index 00000000000..d6e34941f23
--- /dev/null
+++ b/app/assets/images/emoji/clock1.png
Binary files differ
diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png
new file mode 100644
index 00000000000..e62b245cdbe
--- /dev/null
+++ b/app/assets/images/emoji/clock10.png
Binary files differ
diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png
new file mode 100644
index 00000000000..0802b3c65b9
--- /dev/null
+++ b/app/assets/images/emoji/clock1030.png
Binary files differ
diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png
new file mode 100644
index 00000000000..0983345273b
--- /dev/null
+++ b/app/assets/images/emoji/clock11.png
Binary files differ
diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png
new file mode 100644
index 00000000000..d970d03b809
--- /dev/null
+++ b/app/assets/images/emoji/clock1130.png
Binary files differ
diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png
new file mode 100644
index 00000000000..e61caa4b3e2
--- /dev/null
+++ b/app/assets/images/emoji/clock12.png
Binary files differ
diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png
new file mode 100644
index 00000000000..f2b1d261721
--- /dev/null
+++ b/app/assets/images/emoji/clock1230.png
Binary files differ
diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png
new file mode 100644
index 00000000000..86b7689b84e
--- /dev/null
+++ b/app/assets/images/emoji/clock130.png
Binary files differ
diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png
new file mode 100644
index 00000000000..a54253d7d57
--- /dev/null
+++ b/app/assets/images/emoji/clock2.png
Binary files differ
diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png
new file mode 100644
index 00000000000..7a787e018e6
--- /dev/null
+++ b/app/assets/images/emoji/clock230.png
Binary files differ
diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png
new file mode 100644
index 00000000000..27ec4b1f514
--- /dev/null
+++ b/app/assets/images/emoji/clock3.png
Binary files differ
diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png
new file mode 100644
index 00000000000..c6860395cec
--- /dev/null
+++ b/app/assets/images/emoji/clock330.png
Binary files differ
diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png
new file mode 100644
index 00000000000..60a1ef4cc13
--- /dev/null
+++ b/app/assets/images/emoji/clock4.png
Binary files differ
diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png
new file mode 100644
index 00000000000..3c05b362122
--- /dev/null
+++ b/app/assets/images/emoji/clock430.png
Binary files differ
diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png
new file mode 100644
index 00000000000..c9382d1e094
--- /dev/null
+++ b/app/assets/images/emoji/clock5.png
Binary files differ
diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png
new file mode 100644
index 00000000000..c21fa926db2
--- /dev/null
+++ b/app/assets/images/emoji/clock530.png
Binary files differ
diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png
new file mode 100644
index 00000000000..8fd5d3f5bd7
--- /dev/null
+++ b/app/assets/images/emoji/clock6.png
Binary files differ
diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png
new file mode 100644
index 00000000000..2aec87fefcf
--- /dev/null
+++ b/app/assets/images/emoji/clock630.png
Binary files differ
diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png
new file mode 100644
index 00000000000..8c7084036f2
--- /dev/null
+++ b/app/assets/images/emoji/clock7.png
Binary files differ
diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png
new file mode 100644
index 00000000000..f7a1135e03f
--- /dev/null
+++ b/app/assets/images/emoji/clock730.png
Binary files differ
diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png
new file mode 100644
index 00000000000..fcddf722e95
--- /dev/null
+++ b/app/assets/images/emoji/clock8.png
Binary files differ
diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png
new file mode 100644
index 00000000000..799b4aebc08
--- /dev/null
+++ b/app/assets/images/emoji/clock830.png
Binary files differ
diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png
new file mode 100644
index 00000000000..dfbe0117981
--- /dev/null
+++ b/app/assets/images/emoji/clock9.png
Binary files differ
diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png
new file mode 100644
index 00000000000..4a2092ee6f0
--- /dev/null
+++ b/app/assets/images/emoji/clock930.png
Binary files differ
diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png
new file mode 100644
index 00000000000..6395cf2151e
--- /dev/null
+++ b/app/assets/images/emoji/closed_book.png
Binary files differ
diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png
new file mode 100644
index 00000000000..1c1cd5d0741
--- /dev/null
+++ b/app/assets/images/emoji/closed_lock_with_key.png
Binary files differ
diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png
new file mode 100644
index 00000000000..ecefba9e446
--- /dev/null
+++ b/app/assets/images/emoji/closed_umbrella.png
Binary files differ
diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png
new file mode 100644
index 00000000000..5b4f57f77ba
--- /dev/null
+++ b/app/assets/images/emoji/cloud.png
Binary files differ
diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png
new file mode 100644
index 00000000000..0831e88aa31
--- /dev/null
+++ b/app/assets/images/emoji/cloud_lightning.png
Binary files differ
diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png
new file mode 100644
index 00000000000..385685e0512
--- /dev/null
+++ b/app/assets/images/emoji/cloud_rain.png
Binary files differ
diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png
new file mode 100644
index 00000000000..9720384eb99
--- /dev/null
+++ b/app/assets/images/emoji/cloud_snow.png
Binary files differ
diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png
new file mode 100644
index 00000000000..4821c89da1e
--- /dev/null
+++ b/app/assets/images/emoji/cloud_tornado.png
Binary files differ
diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png
new file mode 100644
index 00000000000..02b7ff70049
--- /dev/null
+++ b/app/assets/images/emoji/clown.png
Binary files differ
diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png
new file mode 100644
index 00000000000..4f2abf791ca
--- /dev/null
+++ b/app/assets/images/emoji/clubs.png
Binary files differ
diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png
new file mode 100644
index 00000000000..2e50c57e98d
--- /dev/null
+++ b/app/assets/images/emoji/cocktail.png
Binary files differ
diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png
new file mode 100644
index 00000000000..553061471b1
--- /dev/null
+++ b/app/assets/images/emoji/coffee.png
Binary files differ
diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png
new file mode 100644
index 00000000000..fb2932aa5f6
--- /dev/null
+++ b/app/assets/images/emoji/coffin.png
Binary files differ
diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png
new file mode 100644
index 00000000000..85b2231bbf6
--- /dev/null
+++ b/app/assets/images/emoji/cold_sweat.png
Binary files differ
diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png
new file mode 100644
index 00000000000..a99751f79be
--- /dev/null
+++ b/app/assets/images/emoji/comet.png
Binary files differ
diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png
new file mode 100644
index 00000000000..d7eda7f362a
--- /dev/null
+++ b/app/assets/images/emoji/compression.png
Binary files differ
diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png
new file mode 100644
index 00000000000..c1fee27e3a9
--- /dev/null
+++ b/app/assets/images/emoji/computer.png
Binary files differ
diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png
new file mode 100644
index 00000000000..ba4fd9b12be
--- /dev/null
+++ b/app/assets/images/emoji/confetti_ball.png
Binary files differ
diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png
new file mode 100644
index 00000000000..aa4b29e9375
--- /dev/null
+++ b/app/assets/images/emoji/confounded.png
Binary files differ
diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png
new file mode 100644
index 00000000000..502b6bf0e0b
--- /dev/null
+++ b/app/assets/images/emoji/confused.png
Binary files differ
diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png
new file mode 100644
index 00000000000..ba8c89d95ee
--- /dev/null
+++ b/app/assets/images/emoji/congratulations.png
Binary files differ
diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png
new file mode 100644
index 00000000000..ef8db5f471c
--- /dev/null
+++ b/app/assets/images/emoji/construction.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png
new file mode 100644
index 00000000000..8206a20f63f
--- /dev/null
+++ b/app/assets/images/emoji/construction_site.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png
new file mode 100644
index 00000000000..a9970a89005
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png
new file mode 100644
index 00000000000..2f24a2bab24
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png
new file mode 100644
index 00000000000..93c8fec5a75
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png
new file mode 100644
index 00000000000..abc1f2af2e0
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png
new file mode 100644
index 00000000000..eed83289aeb
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png
new file mode 100644
index 00000000000..acbb220b8bb
--- /dev/null
+++ b/app/assets/images/emoji/construction_worker_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png
new file mode 100644
index 00000000000..6635ac93b50
--- /dev/null
+++ b/app/assets/images/emoji/control_knobs.png
Binary files differ
diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png
new file mode 100644
index 00000000000..26b53b5669e
--- /dev/null
+++ b/app/assets/images/emoji/convenience_store.png
Binary files differ
diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png
new file mode 100644
index 00000000000..1b6bcb1554f
--- /dev/null
+++ b/app/assets/images/emoji/cookie.png
Binary files differ
diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png
new file mode 100644
index 00000000000..918c980577a
--- /dev/null
+++ b/app/assets/images/emoji/cooking.png
Binary files differ
diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png
new file mode 100644
index 00000000000..74674978d00
--- /dev/null
+++ b/app/assets/images/emoji/cool.png
Binary files differ
diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png
new file mode 100644
index 00000000000..0b16d7c17b7
--- /dev/null
+++ b/app/assets/images/emoji/cop.png
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png
new file mode 100644
index 00000000000..6ccba3879dc
--- /dev/null
+++ b/app/assets/images/emoji/cop_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png
new file mode 100644
index 00000000000..7814ea9f52d
--- /dev/null
+++ b/app/assets/images/emoji/cop_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png
new file mode 100644
index 00000000000..d78e88ec872
--- /dev/null
+++ b/app/assets/images/emoji/cop_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png
new file mode 100644
index 00000000000..2e13c508315
--- /dev/null
+++ b/app/assets/images/emoji/cop_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png
new file mode 100644
index 00000000000..2980d61cc2e
--- /dev/null
+++ b/app/assets/images/emoji/cop_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png
new file mode 100644
index 00000000000..6b9a6adbfd2
--- /dev/null
+++ b/app/assets/images/emoji/copyright.png
Binary files differ
diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png
new file mode 100644
index 00000000000..36e20127931
--- /dev/null
+++ b/app/assets/images/emoji/corn.png
Binary files differ
diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png
new file mode 100644
index 00000000000..27b19b13bb0
--- /dev/null
+++ b/app/assets/images/emoji/couch.png
Binary files differ
diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png
new file mode 100644
index 00000000000..960323f3c16
--- /dev/null
+++ b/app/assets/images/emoji/couple.png
Binary files differ
diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png
new file mode 100644
index 00000000000..8759fa5db87
--- /dev/null
+++ b/app/assets/images/emoji/couple_mm.png
Binary files differ
diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png
new file mode 100644
index 00000000000..62111601b36
--- /dev/null
+++ b/app/assets/images/emoji/couple_with_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png
new file mode 100644
index 00000000000..08fdabcdc5c
--- /dev/null
+++ b/app/assets/images/emoji/couple_ww.png
Binary files differ
diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png
new file mode 100644
index 00000000000..9aa519da9e8
--- /dev/null
+++ b/app/assets/images/emoji/couplekiss.png
Binary files differ
diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png
new file mode 100644
index 00000000000..718a3986d64
--- /dev/null
+++ b/app/assets/images/emoji/cow.png
Binary files differ
diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png
new file mode 100644
index 00000000000..4d0ca534ff1
--- /dev/null
+++ b/app/assets/images/emoji/cow2.png
Binary files differ
diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png
new file mode 100644
index 00000000000..70dd5d0d9d1
--- /dev/null
+++ b/app/assets/images/emoji/cowboy.png
Binary files differ
diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png
new file mode 100644
index 00000000000..19f3047ab61
--- /dev/null
+++ b/app/assets/images/emoji/crab.png
Binary files differ
diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png
new file mode 100644
index 00000000000..8d7b427aaa3
--- /dev/null
+++ b/app/assets/images/emoji/crayon.png
Binary files differ
diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png
new file mode 100644
index 00000000000..372777d5c61
--- /dev/null
+++ b/app/assets/images/emoji/credit_card.png
Binary files differ
diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png
new file mode 100644
index 00000000000..765420ecec7
--- /dev/null
+++ b/app/assets/images/emoji/crescent_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png
new file mode 100644
index 00000000000..d602294a2cd
--- /dev/null
+++ b/app/assets/images/emoji/cricket.png
Binary files differ
diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png
new file mode 100644
index 00000000000..3005c46f176
--- /dev/null
+++ b/app/assets/images/emoji/crocodile.png
Binary files differ
diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png
new file mode 100644
index 00000000000..fb33feb1a38
--- /dev/null
+++ b/app/assets/images/emoji/croissant.png
Binary files differ
diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png
new file mode 100644
index 00000000000..42b10e82257
--- /dev/null
+++ b/app/assets/images/emoji/cross.png
Binary files differ
diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png
new file mode 100644
index 00000000000..273bd0f0fe5
--- /dev/null
+++ b/app/assets/images/emoji/crossed_flags.png
Binary files differ
diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png
new file mode 100644
index 00000000000..907e9607134
--- /dev/null
+++ b/app/assets/images/emoji/crossed_swords.png
Binary files differ
diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png
new file mode 100644
index 00000000000..93b82d92f04
--- /dev/null
+++ b/app/assets/images/emoji/crown.png
Binary files differ
diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png
new file mode 100644
index 00000000000..19d4acbe40c
--- /dev/null
+++ b/app/assets/images/emoji/cruise_ship.png
Binary files differ
diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png
new file mode 100644
index 00000000000..b7877f8a173
--- /dev/null
+++ b/app/assets/images/emoji/cry.png
Binary files differ
diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png
new file mode 100644
index 00000000000..b4f49715e00
--- /dev/null
+++ b/app/assets/images/emoji/crying_cat_face.png
Binary files differ
diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png
new file mode 100644
index 00000000000..485d5c888f1
--- /dev/null
+++ b/app/assets/images/emoji/crystal_ball.png
Binary files differ
diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png
new file mode 100644
index 00000000000..500807059d2
--- /dev/null
+++ b/app/assets/images/emoji/cucumber.png
Binary files differ
diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png
new file mode 100644
index 00000000000..2df0078ddd1
--- /dev/null
+++ b/app/assets/images/emoji/cupid.png
Binary files differ
diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png
new file mode 100644
index 00000000000..440aa56d50e
--- /dev/null
+++ b/app/assets/images/emoji/curly_loop.png
Binary files differ
diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png
new file mode 100644
index 00000000000..4d46c6050e7
--- /dev/null
+++ b/app/assets/images/emoji/currency_exchange.png
Binary files differ
diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png
new file mode 100644
index 00000000000..69657ca8103
--- /dev/null
+++ b/app/assets/images/emoji/curry.png
Binary files differ
diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png
new file mode 100644
index 00000000000..fa3df67b8f6
--- /dev/null
+++ b/app/assets/images/emoji/custard.png
Binary files differ
diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png
new file mode 100644
index 00000000000..21b7ce2c69e
--- /dev/null
+++ b/app/assets/images/emoji/customs.png
Binary files differ
diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png
new file mode 100644
index 00000000000..ff00b1afe70
--- /dev/null
+++ b/app/assets/images/emoji/cyclone.png
Binary files differ
diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png
new file mode 100644
index 00000000000..66e97b0aa25
--- /dev/null
+++ b/app/assets/images/emoji/dagger.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png
new file mode 100644
index 00000000000..04b166991cb
--- /dev/null
+++ b/app/assets/images/emoji/dancer.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png
new file mode 100644
index 00000000000..2c7b11c3a6e
--- /dev/null
+++ b/app/assets/images/emoji/dancer_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png
new file mode 100644
index 00000000000..cb04b1f907e
--- /dev/null
+++ b/app/assets/images/emoji/dancer_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png
new file mode 100644
index 00000000000..98c5bca7b64
--- /dev/null
+++ b/app/assets/images/emoji/dancer_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png
new file mode 100644
index 00000000000..fdb1e00cbba
--- /dev/null
+++ b/app/assets/images/emoji/dancer_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png
new file mode 100644
index 00000000000..0e34e0e23f0
--- /dev/null
+++ b/app/assets/images/emoji/dancer_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png
new file mode 100644
index 00000000000..67e6ffacb76
--- /dev/null
+++ b/app/assets/images/emoji/dancers.png
Binary files differ
diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png
new file mode 100644
index 00000000000..f73f37b01c7
--- /dev/null
+++ b/app/assets/images/emoji/dango.png
Binary files differ
diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png
new file mode 100644
index 00000000000..b1b6db0acff
--- /dev/null
+++ b/app/assets/images/emoji/dark_sunglasses.png
Binary files differ
diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png
new file mode 100644
index 00000000000..f6704aeb8ba
--- /dev/null
+++ b/app/assets/images/emoji/dart.png
Binary files differ
diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png
new file mode 100644
index 00000000000..064b8525c12
--- /dev/null
+++ b/app/assets/images/emoji/dash.png
Binary files differ
diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png
new file mode 100644
index 00000000000..f05b3da97b8
--- /dev/null
+++ b/app/assets/images/emoji/date.png
Binary files differ
diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png
new file mode 100644
index 00000000000..785fc1c30ea
--- /dev/null
+++ b/app/assets/images/emoji/deciduous_tree.png
Binary files differ
diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png
new file mode 100644
index 00000000000..d8698195ff0
--- /dev/null
+++ b/app/assets/images/emoji/deer.png
Binary files differ
diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png
new file mode 100644
index 00000000000..58867c7a6e1
--- /dev/null
+++ b/app/assets/images/emoji/department_store.png
Binary files differ
diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png
new file mode 100644
index 00000000000..e9966ff8c65
--- /dev/null
+++ b/app/assets/images/emoji/desert.png
Binary files differ
diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png
new file mode 100644
index 00000000000..909bd42b5e1
--- /dev/null
+++ b/app/assets/images/emoji/desktop.png
Binary files differ
diff --git a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
new file mode 100644
index 00000000000..2a22a26d1e2
--- /dev/null
+++ b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
Binary files differ
diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png
new file mode 100644
index 00000000000..1f25f51f97a
--- /dev/null
+++ b/app/assets/images/emoji/diamonds.png
Binary files differ
diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png
new file mode 100644
index 00000000000..efe4e67e23c
--- /dev/null
+++ b/app/assets/images/emoji/disappointed.png
Binary files differ
diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png
new file mode 100644
index 00000000000..aef864d2b3d
--- /dev/null
+++ b/app/assets/images/emoji/disappointed_relieved.png
Binary files differ
diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png
new file mode 100644
index 00000000000..46a7e403f9d
--- /dev/null
+++ b/app/assets/images/emoji/dividers.png
Binary files differ
diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png
new file mode 100644
index 00000000000..85f52efad24
--- /dev/null
+++ b/app/assets/images/emoji/dizzy.png
Binary files differ
diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png
new file mode 100644
index 00000000000..3120316ab5e
--- /dev/null
+++ b/app/assets/images/emoji/dizzy_face.png
Binary files differ
diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png
new file mode 100644
index 00000000000..341d2575f4f
--- /dev/null
+++ b/app/assets/images/emoji/do_not_litter.png
Binary files differ
diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png
new file mode 100644
index 00000000000..281b81d58bd
--- /dev/null
+++ b/app/assets/images/emoji/dog.png
Binary files differ
diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png
new file mode 100644
index 00000000000..976143dbdbe
--- /dev/null
+++ b/app/assets/images/emoji/dog2.png
Binary files differ
diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png
new file mode 100644
index 00000000000..a9904c28293
--- /dev/null
+++ b/app/assets/images/emoji/dollar.png
Binary files differ
diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png
new file mode 100644
index 00000000000..10955615110
--- /dev/null
+++ b/app/assets/images/emoji/dolls.png
Binary files differ
diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png
new file mode 100644
index 00000000000..81434809003
--- /dev/null
+++ b/app/assets/images/emoji/dolphin.png
Binary files differ
diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png
new file mode 100644
index 00000000000..36ae3e27494
--- /dev/null
+++ b/app/assets/images/emoji/door.png
Binary files differ
diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png
new file mode 100644
index 00000000000..0ca4cd0bde8
--- /dev/null
+++ b/app/assets/images/emoji/doughnut.png
Binary files differ
diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png
new file mode 100644
index 00000000000..9580c4917d7
--- /dev/null
+++ b/app/assets/images/emoji/dove.png
Binary files differ
diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png
new file mode 100644
index 00000000000..d6311cf5429
--- /dev/null
+++ b/app/assets/images/emoji/dragon.png
Binary files differ
diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png
new file mode 100644
index 00000000000..3c2720446c6
--- /dev/null
+++ b/app/assets/images/emoji/dragon_face.png
Binary files differ
diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png
new file mode 100644
index 00000000000..a697ca5c57d
--- /dev/null
+++ b/app/assets/images/emoji/dress.png
Binary files differ
diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png
new file mode 100644
index 00000000000..5271637c7c4
--- /dev/null
+++ b/app/assets/images/emoji/dromedary_camel.png
Binary files differ
diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png
new file mode 100644
index 00000000000..a5460532597
--- /dev/null
+++ b/app/assets/images/emoji/drooling_face.png
Binary files differ
diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png
new file mode 100644
index 00000000000..71241ec3061
--- /dev/null
+++ b/app/assets/images/emoji/droplet.png
Binary files differ
diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png
new file mode 100644
index 00000000000..b038727cc99
--- /dev/null
+++ b/app/assets/images/emoji/drum.png
Binary files differ
diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png
new file mode 100644
index 00000000000..74330b77ca3
--- /dev/null
+++ b/app/assets/images/emoji/duck.png
Binary files differ
diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png
new file mode 100644
index 00000000000..045a6f7a08d
--- /dev/null
+++ b/app/assets/images/emoji/dvd.png
Binary files differ
diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png
new file mode 100644
index 00000000000..d22e654a20b
--- /dev/null
+++ b/app/assets/images/emoji/e-mail.png
Binary files differ
diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png
new file mode 100644
index 00000000000..4f277debeef
--- /dev/null
+++ b/app/assets/images/emoji/eagle.png
Binary files differ
diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png
new file mode 100644
index 00000000000..f84f9ff154a
--- /dev/null
+++ b/app/assets/images/emoji/ear.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png
new file mode 100644
index 00000000000..3564d9d643a
--- /dev/null
+++ b/app/assets/images/emoji/ear_of_rice.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png
new file mode 100644
index 00000000000..d09e1e41996
--- /dev/null
+++ b/app/assets/images/emoji/ear_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png
new file mode 100644
index 00000000000..300d60a9948
--- /dev/null
+++ b/app/assets/images/emoji/ear_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png
new file mode 100644
index 00000000000..2a56eebe445
--- /dev/null
+++ b/app/assets/images/emoji/ear_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png
new file mode 100644
index 00000000000..bd270f7763e
--- /dev/null
+++ b/app/assets/images/emoji/ear_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png
new file mode 100644
index 00000000000..b96bb441dff
--- /dev/null
+++ b/app/assets/images/emoji/ear_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png
new file mode 100644
index 00000000000..66c3348c23a
--- /dev/null
+++ b/app/assets/images/emoji/earth_africa.png
Binary files differ
diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png
new file mode 100644
index 00000000000..538c3cddd68
--- /dev/null
+++ b/app/assets/images/emoji/earth_americas.png
Binary files differ
diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png
new file mode 100644
index 00000000000..d8df97fec3c
--- /dev/null
+++ b/app/assets/images/emoji/earth_asia.png
Binary files differ
diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png
new file mode 100644
index 00000000000..c171974d993
--- /dev/null
+++ b/app/assets/images/emoji/egg.png
Binary files differ
diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png
new file mode 100644
index 00000000000..fafd7c1a14c
--- /dev/null
+++ b/app/assets/images/emoji/eggplant.png
Binary files differ
diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png
new file mode 100644
index 00000000000..8c95874d4c5
--- /dev/null
+++ b/app/assets/images/emoji/eight.png
Binary files differ
diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png
new file mode 100644
index 00000000000..820179bda50
--- /dev/null
+++ b/app/assets/images/emoji/eight_pointed_black_star.png
Binary files differ
diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png
new file mode 100644
index 00000000000..3307ffa62ee
--- /dev/null
+++ b/app/assets/images/emoji/eight_spoked_asterisk.png
Binary files differ
diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png
new file mode 100644
index 00000000000..ec5cfc48973
--- /dev/null
+++ b/app/assets/images/emoji/eject.png
Binary files differ
diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png
new file mode 100644
index 00000000000..31d1eb215b4
--- /dev/null
+++ b/app/assets/images/emoji/electric_plug.png
Binary files differ
diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png
new file mode 100644
index 00000000000..b8a6d140595
--- /dev/null
+++ b/app/assets/images/emoji/elephant.png
Binary files differ
diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png
new file mode 100644
index 00000000000..ef3ccd5f367
--- /dev/null
+++ b/app/assets/images/emoji/end.png
Binary files differ
diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png
new file mode 100644
index 00000000000..ec77ac375a4
--- /dev/null
+++ b/app/assets/images/emoji/envelope.png
Binary files differ
diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png
new file mode 100644
index 00000000000..7448a6b7673
--- /dev/null
+++ b/app/assets/images/emoji/envelope_with_arrow.png
Binary files differ
diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png
new file mode 100644
index 00000000000..a49020820e1
--- /dev/null
+++ b/app/assets/images/emoji/euro.png
Binary files differ
diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png
new file mode 100644
index 00000000000..888d11332ce
--- /dev/null
+++ b/app/assets/images/emoji/european_castle.png
Binary files differ
diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png
new file mode 100644
index 00000000000..3745aff8dd2
--- /dev/null
+++ b/app/assets/images/emoji/european_post_office.png
Binary files differ
diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png
new file mode 100644
index 00000000000..f679d8dd772
--- /dev/null
+++ b/app/assets/images/emoji/evergreen_tree.png
Binary files differ
diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png
new file mode 100644
index 00000000000..2c14406422f
--- /dev/null
+++ b/app/assets/images/emoji/exclamation.png
Binary files differ
diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png
new file mode 100644
index 00000000000..2954017f6c2
--- /dev/null
+++ b/app/assets/images/emoji/expressionless.png
Binary files differ
diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png
new file mode 100644
index 00000000000..9d989cdd375
--- /dev/null
+++ b/app/assets/images/emoji/eye.png
Binary files differ
diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png
new file mode 100644
index 00000000000..21bd22bbcce
--- /dev/null
+++ b/app/assets/images/emoji/eye_in_speech_bubble.png
Binary files differ
diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png
new file mode 100644
index 00000000000..865d8274acf
--- /dev/null
+++ b/app/assets/images/emoji/eyeglasses.png
Binary files differ
diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png
new file mode 100644
index 00000000000..2102ada7e09
--- /dev/null
+++ b/app/assets/images/emoji/eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png
new file mode 100644
index 00000000000..defc796cf16
--- /dev/null
+++ b/app/assets/images/emoji/face_palm.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png
new file mode 100644
index 00000000000..2f4b010bb40
--- /dev/null
+++ b/app/assets/images/emoji/face_palm_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png
new file mode 100644
index 00000000000..97fb6831687
--- /dev/null
+++ b/app/assets/images/emoji/face_palm_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png
new file mode 100644
index 00000000000..b5b5c1e5306
--- /dev/null
+++ b/app/assets/images/emoji/face_palm_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png
new file mode 100644
index 00000000000..2840b113483
--- /dev/null
+++ b/app/assets/images/emoji/face_palm_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png
new file mode 100644
index 00000000000..6f070db98be
--- /dev/null
+++ b/app/assets/images/emoji/face_palm_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png
new file mode 100644
index 00000000000..e1d2ddf4a27
--- /dev/null
+++ b/app/assets/images/emoji/factory.png
Binary files differ
diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png
new file mode 100644
index 00000000000..0d60e7bdf2d
--- /dev/null
+++ b/app/assets/images/emoji/fallen_leaf.png
Binary files differ
diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png
new file mode 100644
index 00000000000..26421965791
--- /dev/null
+++ b/app/assets/images/emoji/family.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png
new file mode 100644
index 00000000000..7a2e4e2c491
--- /dev/null
+++ b/app/assets/images/emoji/family_mmb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png
new file mode 100644
index 00000000000..81e6c0fc0ee
--- /dev/null
+++ b/app/assets/images/emoji/family_mmbb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png
new file mode 100644
index 00000000000..932a85e1fe5
--- /dev/null
+++ b/app/assets/images/emoji/family_mmg.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png
new file mode 100644
index 00000000000..41e35166670
--- /dev/null
+++ b/app/assets/images/emoji/family_mmgb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png
new file mode 100644
index 00000000000..8e8ccfe6c7f
--- /dev/null
+++ b/app/assets/images/emoji/family_mmgg.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png
new file mode 100644
index 00000000000..b544fbe573f
--- /dev/null
+++ b/app/assets/images/emoji/family_mwbb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png
new file mode 100644
index 00000000000..71d2681c32a
--- /dev/null
+++ b/app/assets/images/emoji/family_mwg.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png
new file mode 100644
index 00000000000..40dbf1f7a18
--- /dev/null
+++ b/app/assets/images/emoji/family_mwgb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png
new file mode 100644
index 00000000000..bfefa4879cb
--- /dev/null
+++ b/app/assets/images/emoji/family_mwgg.png
Binary files differ
diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png
new file mode 100644
index 00000000000..836feae7c78
--- /dev/null
+++ b/app/assets/images/emoji/family_wwb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png
new file mode 100644
index 00000000000..6c6ba45e7bb
--- /dev/null
+++ b/app/assets/images/emoji/family_wwbb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png
new file mode 100644
index 00000000000..41225c6fa5a
--- /dev/null
+++ b/app/assets/images/emoji/family_wwg.png
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png
new file mode 100644
index 00000000000..284d29ab5da
--- /dev/null
+++ b/app/assets/images/emoji/family_wwgb.png
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png
new file mode 100644
index 00000000000..d8d3f49b85f
--- /dev/null
+++ b/app/assets/images/emoji/family_wwgg.png
Binary files differ
diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png
new file mode 100644
index 00000000000..c406fedfdb1
--- /dev/null
+++ b/app/assets/images/emoji/fast_forward.png
Binary files differ
diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png
new file mode 100644
index 00000000000..6f929e294c2
--- /dev/null
+++ b/app/assets/images/emoji/fax.png
Binary files differ
diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png
new file mode 100644
index 00000000000..eb8b347cef9
--- /dev/null
+++ b/app/assets/images/emoji/fearful.png
Binary files differ
diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png
new file mode 100644
index 00000000000..5fe568cee93
--- /dev/null
+++ b/app/assets/images/emoji/feet.png
Binary files differ
diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png
new file mode 100644
index 00000000000..5288c920eb9
--- /dev/null
+++ b/app/assets/images/emoji/fencer.png
Binary files differ
diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png
new file mode 100644
index 00000000000..55c8ff0475b
--- /dev/null
+++ b/app/assets/images/emoji/ferris_wheel.png
Binary files differ
diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png
new file mode 100644
index 00000000000..41816b3ae34
--- /dev/null
+++ b/app/assets/images/emoji/ferry.png
Binary files differ
diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png
new file mode 100644
index 00000000000..839637716ee
--- /dev/null
+++ b/app/assets/images/emoji/field_hockey.png
Binary files differ
diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png
new file mode 100644
index 00000000000..fddc65dde96
--- /dev/null
+++ b/app/assets/images/emoji/file_cabinet.png
Binary files differ
diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png
new file mode 100644
index 00000000000..addedaf0870
--- /dev/null
+++ b/app/assets/images/emoji/file_folder.png
Binary files differ
diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png
new file mode 100644
index 00000000000..30143aedbe6
--- /dev/null
+++ b/app/assets/images/emoji/film_frames.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png
new file mode 100644
index 00000000000..4cd18514ea3
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png
new file mode 100644
index 00000000000..dd2384a6cd5
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png
new file mode 100644
index 00000000000..6228401befe
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png
new file mode 100644
index 00000000000..b1074da15f5
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png
new file mode 100644
index 00000000000..75e05e4d332
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png
new file mode 100644
index 00000000000..761aebdc30f
--- /dev/null
+++ b/app/assets/images/emoji/fingers_crossed_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png
new file mode 100644
index 00000000000..bd3775a460b
--- /dev/null
+++ b/app/assets/images/emoji/fire.png
Binary files differ
diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png
new file mode 100644
index 00000000000..2cd45b7cf7e
--- /dev/null
+++ b/app/assets/images/emoji/fire_engine.png
Binary files differ
diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png
new file mode 100644
index 00000000000..176c8b58265
--- /dev/null
+++ b/app/assets/images/emoji/fireworks.png
Binary files differ
diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png
new file mode 100644
index 00000000000..15612b66492
--- /dev/null
+++ b/app/assets/images/emoji/first_place.png
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png
new file mode 100644
index 00000000000..5dccaf72a4f
--- /dev/null
+++ b/app/assets/images/emoji/first_quarter_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png
new file mode 100644
index 00000000000..cd8a3d7acd8
--- /dev/null
+++ b/app/assets/images/emoji/first_quarter_moon_with_face.png
Binary files differ
diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png
new file mode 100644
index 00000000000..c2d2faaacd4
--- /dev/null
+++ b/app/assets/images/emoji/fish.png
Binary files differ
diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png
new file mode 100644
index 00000000000..157bded65db
--- /dev/null
+++ b/app/assets/images/emoji/fish_cake.png
Binary files differ
diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png
new file mode 100644
index 00000000000..dfcdf07eb50
--- /dev/null
+++ b/app/assets/images/emoji/fishing_pole_and_fish.png
Binary files differ
diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png
new file mode 100644
index 00000000000..de33592bf98
--- /dev/null
+++ b/app/assets/images/emoji/fist.png
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png
new file mode 100644
index 00000000000..02809e2dd68
--- /dev/null
+++ b/app/assets/images/emoji/fist_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png
new file mode 100644
index 00000000000..5de34810383
--- /dev/null
+++ b/app/assets/images/emoji/fist_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png
new file mode 100644
index 00000000000..0d5240129b1
--- /dev/null
+++ b/app/assets/images/emoji/fist_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png
new file mode 100644
index 00000000000..a95c0dd634b
--- /dev/null
+++ b/app/assets/images/emoji/fist_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png
new file mode 100644
index 00000000000..a2f092fd8c7
--- /dev/null
+++ b/app/assets/images/emoji/fist_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png
new file mode 100644
index 00000000000..d14371f3f27
--- /dev/null
+++ b/app/assets/images/emoji/five.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png
new file mode 100644
index 00000000000..286239920c7
--- /dev/null
+++ b/app/assets/images/emoji/flag_ac.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png
new file mode 100644
index 00000000000..20f4b14e8ad
--- /dev/null
+++ b/app/assets/images/emoji/flag_ad.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png
new file mode 100644
index 00000000000..d16ffe4b862
--- /dev/null
+++ b/app/assets/images/emoji/flag_ae.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png
new file mode 100644
index 00000000000..a51533b554d
--- /dev/null
+++ b/app/assets/images/emoji/flag_af.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png
new file mode 100644
index 00000000000..07f2ce397d0
--- /dev/null
+++ b/app/assets/images/emoji/flag_ag.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png
new file mode 100644
index 00000000000..500b5ab09fb
--- /dev/null
+++ b/app/assets/images/emoji/flag_ai.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png
new file mode 100644
index 00000000000..03a20132cc6
--- /dev/null
+++ b/app/assets/images/emoji/flag_al.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png
new file mode 100644
index 00000000000..2ad60a273ec
--- /dev/null
+++ b/app/assets/images/emoji/flag_am.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png
new file mode 100644
index 00000000000..cb46c31f862
--- /dev/null
+++ b/app/assets/images/emoji/flag_ao.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png
new file mode 100644
index 00000000000..b272021d375
--- /dev/null
+++ b/app/assets/images/emoji/flag_aq.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png
new file mode 100644
index 00000000000..73136caf3b7
--- /dev/null
+++ b/app/assets/images/emoji/flag_ar.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png
new file mode 100644
index 00000000000..3db45a0d9f3
--- /dev/null
+++ b/app/assets/images/emoji/flag_as.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png
new file mode 100644
index 00000000000..c43769dcb19
--- /dev/null
+++ b/app/assets/images/emoji/flag_at.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png
new file mode 100644
index 00000000000..7794309c78c
--- /dev/null
+++ b/app/assets/images/emoji/flag_au.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png
new file mode 100644
index 00000000000..02c840d12c9
--- /dev/null
+++ b/app/assets/images/emoji/flag_aw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png
new file mode 100644
index 00000000000..fc5466174bb
--- /dev/null
+++ b/app/assets/images/emoji/flag_ax.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png
new file mode 100644
index 00000000000..89d3d15fd9f
--- /dev/null
+++ b/app/assets/images/emoji/flag_az.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png
new file mode 100644
index 00000000000..25fe407e13c
--- /dev/null
+++ b/app/assets/images/emoji/flag_ba.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png
new file mode 100644
index 00000000000..bccd8c5c9b0
--- /dev/null
+++ b/app/assets/images/emoji/flag_bb.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png
new file mode 100644
index 00000000000..b0597a3149b
--- /dev/null
+++ b/app/assets/images/emoji/flag_bd.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png
new file mode 100644
index 00000000000..551f086e3c4
--- /dev/null
+++ b/app/assets/images/emoji/flag_be.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png
new file mode 100644
index 00000000000..444d4829f94
--- /dev/null
+++ b/app/assets/images/emoji/flag_bf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png
new file mode 100644
index 00000000000..821eee5e170
--- /dev/null
+++ b/app/assets/images/emoji/flag_bg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png
new file mode 100644
index 00000000000..f33724249f0
--- /dev/null
+++ b/app/assets/images/emoji/flag_bh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png
new file mode 100644
index 00000000000..ea20ac93211
--- /dev/null
+++ b/app/assets/images/emoji/flag_bi.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png
new file mode 100644
index 00000000000..7cca4f80457
--- /dev/null
+++ b/app/assets/images/emoji/flag_bj.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png
new file mode 100644
index 00000000000..1082e78999f
--- /dev/null
+++ b/app/assets/images/emoji/flag_bl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png
new file mode 100644
index 00000000000..0e28d05d5ac
--- /dev/null
+++ b/app/assets/images/emoji/flag_black.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png
new file mode 100644
index 00000000000..ab8cafdac63
--- /dev/null
+++ b/app/assets/images/emoji/flag_bm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png
new file mode 100644
index 00000000000..caa9329a896
--- /dev/null
+++ b/app/assets/images/emoji/flag_bn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png
new file mode 100644
index 00000000000..98af62b3da7
--- /dev/null
+++ b/app/assets/images/emoji/flag_bo.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png
new file mode 100644
index 00000000000..cb978ef9de9
--- /dev/null
+++ b/app/assets/images/emoji/flag_bq.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png
new file mode 100644
index 00000000000..b139366a42b
--- /dev/null
+++ b/app/assets/images/emoji/flag_br.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png
new file mode 100644
index 00000000000..d36bcd2fb52
--- /dev/null
+++ b/app/assets/images/emoji/flag_bs.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png
new file mode 100644
index 00000000000..ed57aa0360e
--- /dev/null
+++ b/app/assets/images/emoji/flag_bt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png
new file mode 100644
index 00000000000..5884e648228
--- /dev/null
+++ b/app/assets/images/emoji/flag_bv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png
new file mode 100644
index 00000000000..cb12f34739d
--- /dev/null
+++ b/app/assets/images/emoji/flag_bw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png
new file mode 100644
index 00000000000..859c05beb13
--- /dev/null
+++ b/app/assets/images/emoji/flag_by.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png
new file mode 100644
index 00000000000..34761cd03d8
--- /dev/null
+++ b/app/assets/images/emoji/flag_bz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png
new file mode 100644
index 00000000000..7c5b390e85b
--- /dev/null
+++ b/app/assets/images/emoji/flag_ca.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png
new file mode 100644
index 00000000000..b6555a23d83
--- /dev/null
+++ b/app/assets/images/emoji/flag_cc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png
new file mode 100644
index 00000000000..fa92009771d
--- /dev/null
+++ b/app/assets/images/emoji/flag_cd.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png
new file mode 100644
index 00000000000..b969ae29ea9
--- /dev/null
+++ b/app/assets/images/emoji/flag_cf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png
new file mode 100644
index 00000000000..3a38a40a95e
--- /dev/null
+++ b/app/assets/images/emoji/flag_cg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png
new file mode 100644
index 00000000000..5ff86b8a3b7
--- /dev/null
+++ b/app/assets/images/emoji/flag_ch.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png
new file mode 100644
index 00000000000..e3b4d15c7f1
--- /dev/null
+++ b/app/assets/images/emoji/flag_ci.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png
new file mode 100644
index 00000000000..b6b53dbc1c4
--- /dev/null
+++ b/app/assets/images/emoji/flag_ck.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png
new file mode 100644
index 00000000000..c9390da5499
--- /dev/null
+++ b/app/assets/images/emoji/flag_cl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png
new file mode 100644
index 00000000000..2d3f6ec4518
--- /dev/null
+++ b/app/assets/images/emoji/flag_cm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png
new file mode 100644
index 00000000000..0a7f350a6d2
--- /dev/null
+++ b/app/assets/images/emoji/flag_cn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png
new file mode 100644
index 00000000000..7e0f5e0dc3c
--- /dev/null
+++ b/app/assets/images/emoji/flag_co.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png
new file mode 100644
index 00000000000..70c761036bd
--- /dev/null
+++ b/app/assets/images/emoji/flag_cp.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png
new file mode 100644
index 00000000000..a5fce126515
--- /dev/null
+++ b/app/assets/images/emoji/flag_cr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png
new file mode 100644
index 00000000000..447328f7dfd
--- /dev/null
+++ b/app/assets/images/emoji/flag_cu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png
new file mode 100644
index 00000000000..43faf4d64d5
--- /dev/null
+++ b/app/assets/images/emoji/flag_cv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png
new file mode 100644
index 00000000000..eb39e8d0078
--- /dev/null
+++ b/app/assets/images/emoji/flag_cw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png
new file mode 100644
index 00000000000..09d21359f3a
--- /dev/null
+++ b/app/assets/images/emoji/flag_cx.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png
new file mode 100644
index 00000000000..154a7aa3176
--- /dev/null
+++ b/app/assets/images/emoji/flag_cy.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png
new file mode 100644
index 00000000000..9737ca223c7
--- /dev/null
+++ b/app/assets/images/emoji/flag_cz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png
new file mode 100644
index 00000000000..98ed76b3bab
--- /dev/null
+++ b/app/assets/images/emoji/flag_de.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png
new file mode 100644
index 00000000000..aae927d14b8
--- /dev/null
+++ b/app/assets/images/emoji/flag_dg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png
new file mode 100644
index 00000000000..73c2a2acbd9
--- /dev/null
+++ b/app/assets/images/emoji/flag_dj.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png
new file mode 100644
index 00000000000..e5a60b06256
--- /dev/null
+++ b/app/assets/images/emoji/flag_dk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png
new file mode 100644
index 00000000000..50f8a53981d
--- /dev/null
+++ b/app/assets/images/emoji/flag_dm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png
new file mode 100644
index 00000000000..037a45d7c26
--- /dev/null
+++ b/app/assets/images/emoji/flag_do.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png
new file mode 100644
index 00000000000..24945b10f2d
--- /dev/null
+++ b/app/assets/images/emoji/flag_dz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png
new file mode 100644
index 00000000000..356ff347838
--- /dev/null
+++ b/app/assets/images/emoji/flag_ea.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png
new file mode 100644
index 00000000000..13814594619
--- /dev/null
+++ b/app/assets/images/emoji/flag_ec.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png
new file mode 100644
index 00000000000..84f317e7747
--- /dev/null
+++ b/app/assets/images/emoji/flag_ee.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png
new file mode 100644
index 00000000000..57786064a95
--- /dev/null
+++ b/app/assets/images/emoji/flag_eg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png
new file mode 100644
index 00000000000..4d7a76687f6
--- /dev/null
+++ b/app/assets/images/emoji/flag_eh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png
new file mode 100644
index 00000000000..0c3c724c1fb
--- /dev/null
+++ b/app/assets/images/emoji/flag_er.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png
new file mode 100644
index 00000000000..3e73597a225
--- /dev/null
+++ b/app/assets/images/emoji/flag_es.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png
new file mode 100644
index 00000000000..9560a134c97
--- /dev/null
+++ b/app/assets/images/emoji/flag_et.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png
new file mode 100644
index 00000000000..0b456cf3330
--- /dev/null
+++ b/app/assets/images/emoji/flag_eu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png
new file mode 100644
index 00000000000..ebcf58abfc5
--- /dev/null
+++ b/app/assets/images/emoji/flag_fi.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png
new file mode 100644
index 00000000000..9cc8c37fe37
--- /dev/null
+++ b/app/assets/images/emoji/flag_fj.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png
new file mode 100644
index 00000000000..61372fd2549
--- /dev/null
+++ b/app/assets/images/emoji/flag_fk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png
new file mode 100644
index 00000000000..0889825c8e1
--- /dev/null
+++ b/app/assets/images/emoji/flag_fm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png
new file mode 100644
index 00000000000..9a4431b0831
--- /dev/null
+++ b/app/assets/images/emoji/flag_fo.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png
new file mode 100644
index 00000000000..62ca19c3fcf
--- /dev/null
+++ b/app/assets/images/emoji/flag_fr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png
new file mode 100644
index 00000000000..2e68e527a3e
--- /dev/null
+++ b/app/assets/images/emoji/flag_ga.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png
new file mode 100644
index 00000000000..3ed10f62347
--- /dev/null
+++ b/app/assets/images/emoji/flag_gb.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png
new file mode 100644
index 00000000000..527aad33807
--- /dev/null
+++ b/app/assets/images/emoji/flag_gd.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png
new file mode 100644
index 00000000000..a75d142480d
--- /dev/null
+++ b/app/assets/images/emoji/flag_ge.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png
new file mode 100644
index 00000000000..0cf96f327c0
--- /dev/null
+++ b/app/assets/images/emoji/flag_gf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png
new file mode 100644
index 00000000000..970002c7f76
--- /dev/null
+++ b/app/assets/images/emoji/flag_gg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png
new file mode 100644
index 00000000000..f31b5eb7b45
--- /dev/null
+++ b/app/assets/images/emoji/flag_gh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png
new file mode 100644
index 00000000000..e554a2a1d0c
--- /dev/null
+++ b/app/assets/images/emoji/flag_gi.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png
new file mode 100644
index 00000000000..2e795dd4e33
--- /dev/null
+++ b/app/assets/images/emoji/flag_gl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png
new file mode 100644
index 00000000000..bb69c0975a3
--- /dev/null
+++ b/app/assets/images/emoji/flag_gm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png
new file mode 100644
index 00000000000..1981f61dbf5
--- /dev/null
+++ b/app/assets/images/emoji/flag_gn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png
new file mode 100644
index 00000000000..10e42e672bd
--- /dev/null
+++ b/app/assets/images/emoji/flag_gp.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png
new file mode 100644
index 00000000000..11475e61eeb
--- /dev/null
+++ b/app/assets/images/emoji/flag_gq.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png
new file mode 100644
index 00000000000..0f6bb1b6b94
--- /dev/null
+++ b/app/assets/images/emoji/flag_gr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png
new file mode 100644
index 00000000000..6fc92780453
--- /dev/null
+++ b/app/assets/images/emoji/flag_gs.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png
new file mode 100644
index 00000000000..7213d4139ed
--- /dev/null
+++ b/app/assets/images/emoji/flag_gt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png
new file mode 100644
index 00000000000..4027549ca3c
--- /dev/null
+++ b/app/assets/images/emoji/flag_gu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png
new file mode 100644
index 00000000000..6357f6225f4
--- /dev/null
+++ b/app/assets/images/emoji/flag_gw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png
new file mode 100644
index 00000000000..746e2fb7e44
--- /dev/null
+++ b/app/assets/images/emoji/flag_gy.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png
new file mode 100644
index 00000000000..cf0c7151b56
--- /dev/null
+++ b/app/assets/images/emoji/flag_hk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png
new file mode 100644
index 00000000000..b613509e466
--- /dev/null
+++ b/app/assets/images/emoji/flag_hm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png
new file mode 100644
index 00000000000..402cdcefdf8
--- /dev/null
+++ b/app/assets/images/emoji/flag_hn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png
new file mode 100644
index 00000000000..46f4f06b4f2
--- /dev/null
+++ b/app/assets/images/emoji/flag_hr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png
new file mode 100644
index 00000000000..d8d0c888498
--- /dev/null
+++ b/app/assets/images/emoji/flag_ht.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png
new file mode 100644
index 00000000000..a898de636a5
--- /dev/null
+++ b/app/assets/images/emoji/flag_hu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png
new file mode 100644
index 00000000000..69fd990aa95
--- /dev/null
+++ b/app/assets/images/emoji/flag_ic.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png
new file mode 100644
index 00000000000..85b4c063a45
--- /dev/null
+++ b/app/assets/images/emoji/flag_id.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png
new file mode 100644
index 00000000000..a28295838cc
--- /dev/null
+++ b/app/assets/images/emoji/flag_ie.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png
new file mode 100644
index 00000000000..85c410d45fb
--- /dev/null
+++ b/app/assets/images/emoji/flag_il.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png
new file mode 100644
index 00000000000..60a2458e38e
--- /dev/null
+++ b/app/assets/images/emoji/flag_im.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png
new file mode 100644
index 00000000000..feccc8952ce
--- /dev/null
+++ b/app/assets/images/emoji/flag_in.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png
new file mode 100644
index 00000000000..aae927d14b8
--- /dev/null
+++ b/app/assets/images/emoji/flag_io.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png
new file mode 100644
index 00000000000..41fd1db6f86
--- /dev/null
+++ b/app/assets/images/emoji/flag_iq.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png
new file mode 100644
index 00000000000..ff7aaf62ba6
--- /dev/null
+++ b/app/assets/images/emoji/flag_ir.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png
new file mode 100644
index 00000000000..ad8d4131dd2
--- /dev/null
+++ b/app/assets/images/emoji/flag_is.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png
new file mode 100644
index 00000000000..f21563ec533
--- /dev/null
+++ b/app/assets/images/emoji/flag_it.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png
new file mode 100644
index 00000000000..198a918f6a4
--- /dev/null
+++ b/app/assets/images/emoji/flag_je.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png
new file mode 100644
index 00000000000..f84e4f9e8db
--- /dev/null
+++ b/app/assets/images/emoji/flag_jm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png
new file mode 100644
index 00000000000..20bfa147e3e
--- /dev/null
+++ b/app/assets/images/emoji/flag_jo.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png
new file mode 100644
index 00000000000..8d8838e4708
--- /dev/null
+++ b/app/assets/images/emoji/flag_jp.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png
new file mode 100644
index 00000000000..9e417ab3009
--- /dev/null
+++ b/app/assets/images/emoji/flag_ke.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png
new file mode 100644
index 00000000000..2f2d848fe58
--- /dev/null
+++ b/app/assets/images/emoji/flag_kg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png
new file mode 100644
index 00000000000..9a2877dd620
--- /dev/null
+++ b/app/assets/images/emoji/flag_kh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png
new file mode 100644
index 00000000000..10e507e3245
--- /dev/null
+++ b/app/assets/images/emoji/flag_ki.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png
new file mode 100644
index 00000000000..bd5a0588e03
--- /dev/null
+++ b/app/assets/images/emoji/flag_km.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png
new file mode 100644
index 00000000000..776207c9605
--- /dev/null
+++ b/app/assets/images/emoji/flag_kn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png
new file mode 100644
index 00000000000..6b3fd89eaaa
--- /dev/null
+++ b/app/assets/images/emoji/flag_kp.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png
new file mode 100644
index 00000000000..833a88116e1
--- /dev/null
+++ b/app/assets/images/emoji/flag_kr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png
new file mode 100644
index 00000000000..4d19bfa6ca7
--- /dev/null
+++ b/app/assets/images/emoji/flag_kw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png
new file mode 100644
index 00000000000..40daa4da597
--- /dev/null
+++ b/app/assets/images/emoji/flag_ky.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png
new file mode 100644
index 00000000000..2f97a8fd3c6
--- /dev/null
+++ b/app/assets/images/emoji/flag_kz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png
new file mode 100644
index 00000000000..4d4179f34f6
--- /dev/null
+++ b/app/assets/images/emoji/flag_la.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png
new file mode 100644
index 00000000000..3d594467011
--- /dev/null
+++ b/app/assets/images/emoji/flag_lb.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png
new file mode 100644
index 00000000000..45547b1e439
--- /dev/null
+++ b/app/assets/images/emoji/flag_lc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png
new file mode 100644
index 00000000000..0eafa6a2215
--- /dev/null
+++ b/app/assets/images/emoji/flag_li.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png
new file mode 100644
index 00000000000..ab4fe10c40c
--- /dev/null
+++ b/app/assets/images/emoji/flag_lk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png
new file mode 100644
index 00000000000..f66f267fea2
--- /dev/null
+++ b/app/assets/images/emoji/flag_lr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png
new file mode 100644
index 00000000000..24745631e3c
--- /dev/null
+++ b/app/assets/images/emoji/flag_ls.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png
new file mode 100644
index 00000000000..d644b56d62a
--- /dev/null
+++ b/app/assets/images/emoji/flag_lt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png
new file mode 100644
index 00000000000..a2df9c92994
--- /dev/null
+++ b/app/assets/images/emoji/flag_lu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png
new file mode 100644
index 00000000000..ae680d5f0e3
--- /dev/null
+++ b/app/assets/images/emoji/flag_lv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png
new file mode 100644
index 00000000000..f6e77b0f3ba
--- /dev/null
+++ b/app/assets/images/emoji/flag_ly.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png
new file mode 100644
index 00000000000..c4a056722cd
--- /dev/null
+++ b/app/assets/images/emoji/flag_ma.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png
new file mode 100644
index 00000000000..d479eab98cb
--- /dev/null
+++ b/app/assets/images/emoji/flag_mc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png
new file mode 100644
index 00000000000..a7a72539872
--- /dev/null
+++ b/app/assets/images/emoji/flag_md.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png
new file mode 100644
index 00000000000..7c771e7e120
--- /dev/null
+++ b/app/assets/images/emoji/flag_me.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png
new file mode 100644
index 00000000000..70c761036bd
--- /dev/null
+++ b/app/assets/images/emoji/flag_mf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png
new file mode 100644
index 00000000000..2f3ccdda76f
--- /dev/null
+++ b/app/assets/images/emoji/flag_mg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png
new file mode 100644
index 00000000000..598016481c1
--- /dev/null
+++ b/app/assets/images/emoji/flag_mh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png
new file mode 100644
index 00000000000..7ba775ee75c
--- /dev/null
+++ b/app/assets/images/emoji/flag_mk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png
new file mode 100644
index 00000000000..68343785468
--- /dev/null
+++ b/app/assets/images/emoji/flag_ml.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png
new file mode 100644
index 00000000000..37dc7d71591
--- /dev/null
+++ b/app/assets/images/emoji/flag_mm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png
new file mode 100644
index 00000000000..1f146bbcd1a
--- /dev/null
+++ b/app/assets/images/emoji/flag_mn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png
new file mode 100644
index 00000000000..7edde31f64b
--- /dev/null
+++ b/app/assets/images/emoji/flag_mo.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png
new file mode 100644
index 00000000000..17ec1c441ed
--- /dev/null
+++ b/app/assets/images/emoji/flag_mp.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png
new file mode 100644
index 00000000000..1e672dc9087
--- /dev/null
+++ b/app/assets/images/emoji/flag_mq.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png
new file mode 100644
index 00000000000..f87de46effe
--- /dev/null
+++ b/app/assets/images/emoji/flag_mr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png
new file mode 100644
index 00000000000..480b0d4ebda
--- /dev/null
+++ b/app/assets/images/emoji/flag_ms.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png
new file mode 100644
index 00000000000..c9e1dbdce82
--- /dev/null
+++ b/app/assets/images/emoji/flag_mt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png
new file mode 100644
index 00000000000..55b33cb7c33
--- /dev/null
+++ b/app/assets/images/emoji/flag_mu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png
new file mode 100644
index 00000000000..ce5867126ae
--- /dev/null
+++ b/app/assets/images/emoji/flag_mv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png
new file mode 100644
index 00000000000..003d8548401
--- /dev/null
+++ b/app/assets/images/emoji/flag_mw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png
new file mode 100644
index 00000000000..42572bcd0ba
--- /dev/null
+++ b/app/assets/images/emoji/flag_mx.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png
new file mode 100644
index 00000000000..17526c26742
--- /dev/null
+++ b/app/assets/images/emoji/flag_my.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png
new file mode 100644
index 00000000000..2352a78e786
--- /dev/null
+++ b/app/assets/images/emoji/flag_mz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png
new file mode 100644
index 00000000000..ed31c3df04d
--- /dev/null
+++ b/app/assets/images/emoji/flag_na.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png
new file mode 100644
index 00000000000..90b3afebfa3
--- /dev/null
+++ b/app/assets/images/emoji/flag_nc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png
new file mode 100644
index 00000000000..f98a1173c2a
--- /dev/null
+++ b/app/assets/images/emoji/flag_ne.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png
new file mode 100644
index 00000000000..9099e767420
--- /dev/null
+++ b/app/assets/images/emoji/flag_nf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png
new file mode 100644
index 00000000000..ea0abeff1a1
--- /dev/null
+++ b/app/assets/images/emoji/flag_ng.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png
new file mode 100644
index 00000000000..772920dfa10
--- /dev/null
+++ b/app/assets/images/emoji/flag_ni.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png
new file mode 100644
index 00000000000..83a0e817e41
--- /dev/null
+++ b/app/assets/images/emoji/flag_nl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png
new file mode 100644
index 00000000000..99d3142eb7b
--- /dev/null
+++ b/app/assets/images/emoji/flag_no.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png
new file mode 100644
index 00000000000..87425a8dfef
--- /dev/null
+++ b/app/assets/images/emoji/flag_np.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png
new file mode 100644
index 00000000000..b3e3a5d5621
--- /dev/null
+++ b/app/assets/images/emoji/flag_nr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png
new file mode 100644
index 00000000000..f03614443ee
--- /dev/null
+++ b/app/assets/images/emoji/flag_nu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png
new file mode 100644
index 00000000000..a4eeeab9cd9
--- /dev/null
+++ b/app/assets/images/emoji/flag_nz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png
new file mode 100644
index 00000000000..ea824ba31e7
--- /dev/null
+++ b/app/assets/images/emoji/flag_om.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png
new file mode 100644
index 00000000000..c3091d89889
--- /dev/null
+++ b/app/assets/images/emoji/flag_pa.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png
new file mode 100644
index 00000000000..39223aa9dbb
--- /dev/null
+++ b/app/assets/images/emoji/flag_pe.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png
new file mode 100644
index 00000000000..113445f8f6e
--- /dev/null
+++ b/app/assets/images/emoji/flag_pf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png
new file mode 100644
index 00000000000..825e9dcb762
--- /dev/null
+++ b/app/assets/images/emoji/flag_pg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png
new file mode 100644
index 00000000000..8260e15bd2c
--- /dev/null
+++ b/app/assets/images/emoji/flag_ph.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png
new file mode 100644
index 00000000000..a7b6a1c5074
--- /dev/null
+++ b/app/assets/images/emoji/flag_pk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png
new file mode 100644
index 00000000000..19de2edec11
--- /dev/null
+++ b/app/assets/images/emoji/flag_pl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png
new file mode 100644
index 00000000000..2ca60554193
--- /dev/null
+++ b/app/assets/images/emoji/flag_pm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png
new file mode 100644
index 00000000000..f2263b154bc
--- /dev/null
+++ b/app/assets/images/emoji/flag_pn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png
new file mode 100644
index 00000000000..d0209cddb79
--- /dev/null
+++ b/app/assets/images/emoji/flag_pr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png
new file mode 100644
index 00000000000..7ccab09778b
--- /dev/null
+++ b/app/assets/images/emoji/flag_ps.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png
new file mode 100644
index 00000000000..cc93f27c64b
--- /dev/null
+++ b/app/assets/images/emoji/flag_pt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png
new file mode 100644
index 00000000000..154b2f12d3c
--- /dev/null
+++ b/app/assets/images/emoji/flag_pw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png
new file mode 100644
index 00000000000..662ad2f6ff1
--- /dev/null
+++ b/app/assets/images/emoji/flag_py.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png
new file mode 100644
index 00000000000..a01d8b05cc7
--- /dev/null
+++ b/app/assets/images/emoji/flag_qa.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png
new file mode 100644
index 00000000000..57f2bbe9df8
--- /dev/null
+++ b/app/assets/images/emoji/flag_re.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png
new file mode 100644
index 00000000000..3e48c447706
--- /dev/null
+++ b/app/assets/images/emoji/flag_ro.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png
new file mode 100644
index 00000000000..9df6c9a5235
--- /dev/null
+++ b/app/assets/images/emoji/flag_rs.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png
new file mode 100644
index 00000000000..e50c9db90e7
--- /dev/null
+++ b/app/assets/images/emoji/flag_ru.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png
new file mode 100644
index 00000000000..c238c874e1d
--- /dev/null
+++ b/app/assets/images/emoji/flag_rw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png
new file mode 100644
index 00000000000..4941be7d198
--- /dev/null
+++ b/app/assets/images/emoji/flag_sa.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png
new file mode 100644
index 00000000000..7d8f1ac6130
--- /dev/null
+++ b/app/assets/images/emoji/flag_sb.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png
new file mode 100644
index 00000000000..6ae4d90765e
--- /dev/null
+++ b/app/assets/images/emoji/flag_sc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png
new file mode 100644
index 00000000000..963be1b36fb
--- /dev/null
+++ b/app/assets/images/emoji/flag_sd.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png
new file mode 100644
index 00000000000..fc0d0e0ce89
--- /dev/null
+++ b/app/assets/images/emoji/flag_se.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png
new file mode 100644
index 00000000000..de3c7737c42
--- /dev/null
+++ b/app/assets/images/emoji/flag_sg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png
new file mode 100644
index 00000000000..40cd9e44e96
--- /dev/null
+++ b/app/assets/images/emoji/flag_sh.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png
new file mode 100644
index 00000000000..e308999dba2
--- /dev/null
+++ b/app/assets/images/emoji/flag_si.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png
new file mode 100644
index 00000000000..5884e648228
--- /dev/null
+++ b/app/assets/images/emoji/flag_sj.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png
new file mode 100644
index 00000000000..4259d0e1418
--- /dev/null
+++ b/app/assets/images/emoji/flag_sk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png
new file mode 100644
index 00000000000..d2cc68830ab
--- /dev/null
+++ b/app/assets/images/emoji/flag_sl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png
new file mode 100644
index 00000000000..03b8708754e
--- /dev/null
+++ b/app/assets/images/emoji/flag_sm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png
new file mode 100644
index 00000000000..5368bbe93df
--- /dev/null
+++ b/app/assets/images/emoji/flag_sn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png
new file mode 100644
index 00000000000..68a0597365a
--- /dev/null
+++ b/app/assets/images/emoji/flag_so.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png
new file mode 100644
index 00000000000..d3251327035
--- /dev/null
+++ b/app/assets/images/emoji/flag_sr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png
new file mode 100644
index 00000000000..122977e798f
--- /dev/null
+++ b/app/assets/images/emoji/flag_ss.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png
new file mode 100644
index 00000000000..f83a863d612
--- /dev/null
+++ b/app/assets/images/emoji/flag_st.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png
new file mode 100644
index 00000000000..efb83e2f253
--- /dev/null
+++ b/app/assets/images/emoji/flag_sv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png
new file mode 100644
index 00000000000..94b760fbedf
--- /dev/null
+++ b/app/assets/images/emoji/flag_sx.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png
new file mode 100644
index 00000000000..09a8ee8f78c
--- /dev/null
+++ b/app/assets/images/emoji/flag_sy.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png
new file mode 100644
index 00000000000..f74e82ea1fd
--- /dev/null
+++ b/app/assets/images/emoji/flag_sz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png
new file mode 100644
index 00000000000..b44283e90e2
--- /dev/null
+++ b/app/assets/images/emoji/flag_ta.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png
new file mode 100644
index 00000000000..156b33d1ba6
--- /dev/null
+++ b/app/assets/images/emoji/flag_tc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png
new file mode 100644
index 00000000000..ebe7f592828
--- /dev/null
+++ b/app/assets/images/emoji/flag_td.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png
new file mode 100644
index 00000000000..a1a3ad68ee2
--- /dev/null
+++ b/app/assets/images/emoji/flag_tf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png
new file mode 100644
index 00000000000..826b73c9ac5
--- /dev/null
+++ b/app/assets/images/emoji/flag_tg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png
new file mode 100644
index 00000000000..93ff542c5a6
--- /dev/null
+++ b/app/assets/images/emoji/flag_th.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png
new file mode 100644
index 00000000000..7a8a0b6190a
--- /dev/null
+++ b/app/assets/images/emoji/flag_tj.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png
new file mode 100644
index 00000000000..2fa5a21b1bb
--- /dev/null
+++ b/app/assets/images/emoji/flag_tk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png
new file mode 100644
index 00000000000..5b120eccc6f
--- /dev/null
+++ b/app/assets/images/emoji/flag_tl.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png
new file mode 100644
index 00000000000..c3c4f532302
--- /dev/null
+++ b/app/assets/images/emoji/flag_tm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png
new file mode 100644
index 00000000000..58ef161229f
--- /dev/null
+++ b/app/assets/images/emoji/flag_tn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png
new file mode 100644
index 00000000000..1ffa7bb9d19
--- /dev/null
+++ b/app/assets/images/emoji/flag_to.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png
new file mode 100644
index 00000000000..325251fae88
--- /dev/null
+++ b/app/assets/images/emoji/flag_tr.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png
new file mode 100644
index 00000000000..ed3bb39a300
--- /dev/null
+++ b/app/assets/images/emoji/flag_tt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png
new file mode 100644
index 00000000000..e82c65c7bb9
--- /dev/null
+++ b/app/assets/images/emoji/flag_tv.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png
new file mode 100644
index 00000000000..3a8f00b5928
--- /dev/null
+++ b/app/assets/images/emoji/flag_tw.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png
new file mode 100644
index 00000000000..2a020853d4e
--- /dev/null
+++ b/app/assets/images/emoji/flag_tz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png
new file mode 100644
index 00000000000..cd84d1bbd36
--- /dev/null
+++ b/app/assets/images/emoji/flag_ua.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png
new file mode 100644
index 00000000000..dc97690eb55
--- /dev/null
+++ b/app/assets/images/emoji/flag_ug.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png
new file mode 100644
index 00000000000..4a7ee3cdf13
--- /dev/null
+++ b/app/assets/images/emoji/flag_um.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png
new file mode 100644
index 00000000000..9f730305860
--- /dev/null
+++ b/app/assets/images/emoji/flag_us.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png
new file mode 100644
index 00000000000..b8002a697a6
--- /dev/null
+++ b/app/assets/images/emoji/flag_uy.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png
new file mode 100644
index 00000000000..d56ca9bc424
--- /dev/null
+++ b/app/assets/images/emoji/flag_uz.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png
new file mode 100644
index 00000000000..ddaf5e3141b
--- /dev/null
+++ b/app/assets/images/emoji/flag_va.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png
new file mode 100644
index 00000000000..43703c62a71
--- /dev/null
+++ b/app/assets/images/emoji/flag_vc.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png
new file mode 100644
index 00000000000..1b62796824e
--- /dev/null
+++ b/app/assets/images/emoji/flag_ve.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png
new file mode 100644
index 00000000000..536f780f1c0
--- /dev/null
+++ b/app/assets/images/emoji/flag_vg.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png
new file mode 100644
index 00000000000..64102012cfe
--- /dev/null
+++ b/app/assets/images/emoji/flag_vi.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png
new file mode 100644
index 00000000000..427036046b6
--- /dev/null
+++ b/app/assets/images/emoji/flag_vn.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png
new file mode 100644
index 00000000000..706eba44070
--- /dev/null
+++ b/app/assets/images/emoji/flag_vu.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png
new file mode 100644
index 00000000000..70c761036bd
--- /dev/null
+++ b/app/assets/images/emoji/flag_wf.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png
new file mode 100644
index 00000000000..86d6e96d5e9
--- /dev/null
+++ b/app/assets/images/emoji/flag_white.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png
new file mode 100644
index 00000000000..a1ea0703141
--- /dev/null
+++ b/app/assets/images/emoji/flag_ws.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png
new file mode 100644
index 00000000000..e587a446632
--- /dev/null
+++ b/app/assets/images/emoji/flag_xk.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png
new file mode 100644
index 00000000000..eadfebd5f67
--- /dev/null
+++ b/app/assets/images/emoji/flag_ye.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png
new file mode 100644
index 00000000000..c81fa6d886e
--- /dev/null
+++ b/app/assets/images/emoji/flag_yt.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png
new file mode 100644
index 00000000000..f397ef5072f
--- /dev/null
+++ b/app/assets/images/emoji/flag_za.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png
new file mode 100644
index 00000000000..2494a31f662
--- /dev/null
+++ b/app/assets/images/emoji/flag_zm.png
Binary files differ
diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png
new file mode 100644
index 00000000000..e09b9652be6
--- /dev/null
+++ b/app/assets/images/emoji/flag_zw.png
Binary files differ
diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png
new file mode 100644
index 00000000000..3b451035a3a
--- /dev/null
+++ b/app/assets/images/emoji/flags.png
Binary files differ
diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png
new file mode 100644
index 00000000000..eee36c25067
--- /dev/null
+++ b/app/assets/images/emoji/flashlight.png
Binary files differ
diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png
new file mode 100644
index 00000000000..c9250d27fa7
--- /dev/null
+++ b/app/assets/images/emoji/fleur-de-lis.png
Binary files differ
diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png
new file mode 100644
index 00000000000..072a76d3c13
--- /dev/null
+++ b/app/assets/images/emoji/floppy_disk.png
Binary files differ
diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png
new file mode 100644
index 00000000000..6766b044d95
--- /dev/null
+++ b/app/assets/images/emoji/flower_playing_cards.png
Binary files differ
diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png
new file mode 100644
index 00000000000..829220bc470
--- /dev/null
+++ b/app/assets/images/emoji/flushed.png
Binary files differ
diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png
new file mode 100644
index 00000000000..4e73c2de272
--- /dev/null
+++ b/app/assets/images/emoji/fog.png
Binary files differ
diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png
new file mode 100644
index 00000000000..57702d8d3ac
--- /dev/null
+++ b/app/assets/images/emoji/foggy.png
Binary files differ
diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png
new file mode 100644
index 00000000000..10366f41fce
--- /dev/null
+++ b/app/assets/images/emoji/football.png
Binary files differ
diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png
new file mode 100644
index 00000000000..b2673c5a1a8
--- /dev/null
+++ b/app/assets/images/emoji/footprints.png
Binary files differ
diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png
new file mode 100644
index 00000000000..09f1feaea1c
--- /dev/null
+++ b/app/assets/images/emoji/fork_and_knife.png
Binary files differ
diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png
new file mode 100644
index 00000000000..7411755f708
--- /dev/null
+++ b/app/assets/images/emoji/fork_knife_plate.png
Binary files differ
diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png
new file mode 100644
index 00000000000..293f5d91c0f
--- /dev/null
+++ b/app/assets/images/emoji/fountain.png
Binary files differ
diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png
new file mode 100644
index 00000000000..b0e914aac45
--- /dev/null
+++ b/app/assets/images/emoji/four.png
Binary files differ
diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png
new file mode 100644
index 00000000000..fdedfcc2b4e
--- /dev/null
+++ b/app/assets/images/emoji/four_leaf_clover.png
Binary files differ
diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png
new file mode 100644
index 00000000000..1ab339bf054
--- /dev/null
+++ b/app/assets/images/emoji/fox.png
Binary files differ
diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png
new file mode 100644
index 00000000000..9fe84607bfd
--- /dev/null
+++ b/app/assets/images/emoji/frame_photo.png
Binary files differ
diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png
new file mode 100644
index 00000000000..b71956eb48a
--- /dev/null
+++ b/app/assets/images/emoji/free.png
Binary files differ
diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png
new file mode 100644
index 00000000000..4c2c5639822
--- /dev/null
+++ b/app/assets/images/emoji/french_bread.png
Binary files differ
diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png
new file mode 100644
index 00000000000..752ba7f1398
--- /dev/null
+++ b/app/assets/images/emoji/fried_shrimp.png
Binary files differ
diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png
new file mode 100644
index 00000000000..4e2a4caacef
--- /dev/null
+++ b/app/assets/images/emoji/fries.png
Binary files differ
diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png
new file mode 100644
index 00000000000..8825d1ad577
--- /dev/null
+++ b/app/assets/images/emoji/frog.png
Binary files differ
diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png
new file mode 100644
index 00000000000..43ab6b0a1c1
--- /dev/null
+++ b/app/assets/images/emoji/frowning.png
Binary files differ
diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png
new file mode 100644
index 00000000000..6ae71f233b9
--- /dev/null
+++ b/app/assets/images/emoji/frowning2.png
Binary files differ
diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png
new file mode 100644
index 00000000000..05b18794474
--- /dev/null
+++ b/app/assets/images/emoji/fuelpump.png
Binary files differ
diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png
new file mode 100644
index 00000000000..c9a2d6aa7c9
--- /dev/null
+++ b/app/assets/images/emoji/full_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png
new file mode 100644
index 00000000000..a5c25bbaf64
--- /dev/null
+++ b/app/assets/images/emoji/full_moon_with_face.png
Binary files differ
diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png
new file mode 100644
index 00000000000..ad3626fe5e5
--- /dev/null
+++ b/app/assets/images/emoji/game_die.png
Binary files differ
diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png
new file mode 100644
index 00000000000..2a1cc2c0ff4
--- /dev/null
+++ b/app/assets/images/emoji/gear.png
Binary files differ
diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png
new file mode 100644
index 00000000000..db122d26a19
--- /dev/null
+++ b/app/assets/images/emoji/gem.png
Binary files differ
diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png
new file mode 100644
index 00000000000..1a09698cf00
--- /dev/null
+++ b/app/assets/images/emoji/gemini.png
Binary files differ
diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png
new file mode 100644
index 00000000000..5650bc0ed18
--- /dev/null
+++ b/app/assets/images/emoji/ghost.png
Binary files differ
diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png
new file mode 100644
index 00000000000..844e2164560
--- /dev/null
+++ b/app/assets/images/emoji/gift.png
Binary files differ
diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png
new file mode 100644
index 00000000000..902ceafe4d1
--- /dev/null
+++ b/app/assets/images/emoji/gift_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png
new file mode 100644
index 00000000000..dc1d4d08b39
--- /dev/null
+++ b/app/assets/images/emoji/girl.png
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png
new file mode 100644
index 00000000000..bb667e88651
--- /dev/null
+++ b/app/assets/images/emoji/girl_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png
new file mode 100644
index 00000000000..a59ed4a3f0d
--- /dev/null
+++ b/app/assets/images/emoji/girl_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png
new file mode 100644
index 00000000000..517e7f2a7b0
--- /dev/null
+++ b/app/assets/images/emoji/girl_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png
new file mode 100644
index 00000000000..542d96c8487
--- /dev/null
+++ b/app/assets/images/emoji/girl_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png
new file mode 100644
index 00000000000..66b7c28c2df
--- /dev/null
+++ b/app/assets/images/emoji/girl_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png
new file mode 100644
index 00000000000..82450c1a4ba
--- /dev/null
+++ b/app/assets/images/emoji/globe_with_meridians.png
Binary files differ
diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png
new file mode 100644
index 00000000000..df3a53da0fb
--- /dev/null
+++ b/app/assets/images/emoji/goal.png
Binary files differ
diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png
new file mode 100644
index 00000000000..f9d9e38a128
--- /dev/null
+++ b/app/assets/images/emoji/goat.png
Binary files differ
diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png
new file mode 100644
index 00000000000..f65a21d8a46
--- /dev/null
+++ b/app/assets/images/emoji/golf.png
Binary files differ
diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png
new file mode 100644
index 00000000000..39c552de86d
--- /dev/null
+++ b/app/assets/images/emoji/golfer.png
Binary files differ
diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png
new file mode 100644
index 00000000000..acc51e13622
--- /dev/null
+++ b/app/assets/images/emoji/gorilla.png
Binary files differ
diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png
new file mode 100644
index 00000000000..30d22218896
--- /dev/null
+++ b/app/assets/images/emoji/grapes.png
Binary files differ
diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png
new file mode 100644
index 00000000000..5fd51bd3915
--- /dev/null
+++ b/app/assets/images/emoji/green_apple.png
Binary files differ
diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png
new file mode 100644
index 00000000000..e5e411cf3b5
--- /dev/null
+++ b/app/assets/images/emoji/green_book.png
Binary files differ
diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png
new file mode 100644
index 00000000000..c52d60a58be
--- /dev/null
+++ b/app/assets/images/emoji/green_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png
new file mode 100644
index 00000000000..9b64da8bf7f
--- /dev/null
+++ b/app/assets/images/emoji/grey_exclamation.png
Binary files differ
diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png
new file mode 100644
index 00000000000..6e7824c75f6
--- /dev/null
+++ b/app/assets/images/emoji/grey_question.png
Binary files differ
diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png
new file mode 100644
index 00000000000..871b2f071c9
--- /dev/null
+++ b/app/assets/images/emoji/grimacing.png
Binary files differ
diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png
new file mode 100644
index 00000000000..418d94c811b
--- /dev/null
+++ b/app/assets/images/emoji/grin.png
Binary files differ
diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png
new file mode 100644
index 00000000000..3e8e0dab78c
--- /dev/null
+++ b/app/assets/images/emoji/grinning.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png
new file mode 100644
index 00000000000..8d7ab3c473c
--- /dev/null
+++ b/app/assets/images/emoji/guardsman.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png
new file mode 100644
index 00000000000..cea9ba27468
--- /dev/null
+++ b/app/assets/images/emoji/guardsman_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png
new file mode 100644
index 00000000000..037464e4028
--- /dev/null
+++ b/app/assets/images/emoji/guardsman_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png
new file mode 100644
index 00000000000..0f6726fbe87
--- /dev/null
+++ b/app/assets/images/emoji/guardsman_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png
new file mode 100644
index 00000000000..85fcf9a3b97
--- /dev/null
+++ b/app/assets/images/emoji/guardsman_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png
new file mode 100644
index 00000000000..e5f9ca7d5a2
--- /dev/null
+++ b/app/assets/images/emoji/guardsman_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png
new file mode 100644
index 00000000000..43d752f1e3d
--- /dev/null
+++ b/app/assets/images/emoji/guitar.png
Binary files differ
diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png
new file mode 100644
index 00000000000..89c5c244c7b
--- /dev/null
+++ b/app/assets/images/emoji/gun.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png
new file mode 100644
index 00000000000..91266b12930
--- /dev/null
+++ b/app/assets/images/emoji/haircut.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png
new file mode 100644
index 00000000000..c743b74abeb
--- /dev/null
+++ b/app/assets/images/emoji/haircut_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png
new file mode 100644
index 00000000000..f144f8e55ce
--- /dev/null
+++ b/app/assets/images/emoji/haircut_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png
new file mode 100644
index 00000000000..d5ad19563ac
--- /dev/null
+++ b/app/assets/images/emoji/haircut_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png
new file mode 100644
index 00000000000..244fd3af008
--- /dev/null
+++ b/app/assets/images/emoji/haircut_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png
new file mode 100644
index 00000000000..20a94a88623
--- /dev/null
+++ b/app/assets/images/emoji/haircut_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png
new file mode 100644
index 00000000000..3573b28a1fd
--- /dev/null
+++ b/app/assets/images/emoji/hamburger.png
Binary files differ
diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png
new file mode 100644
index 00000000000..00736cce47d
--- /dev/null
+++ b/app/assets/images/emoji/hammer.png
Binary files differ
diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png
new file mode 100644
index 00000000000..3bee30ec588
--- /dev/null
+++ b/app/assets/images/emoji/hammer_pick.png
Binary files differ
diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png
new file mode 100644
index 00000000000..9a04388e4e7
--- /dev/null
+++ b/app/assets/images/emoji/hamster.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png
new file mode 100644
index 00000000000..fb5ae8ebb5a
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png
new file mode 100644
index 00000000000..a7888e6bd23
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png
new file mode 100644
index 00000000000..cc10fbc272d
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png
new file mode 100644
index 00000000000..707236ae8a4
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png
new file mode 100644
index 00000000000..1430df9c61f
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png
new file mode 100644
index 00000000000..80bec971b6b
--- /dev/null
+++ b/app/assets/images/emoji/hand_splayed_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png
new file mode 100644
index 00000000000..cbf75c5d25e
--- /dev/null
+++ b/app/assets/images/emoji/handbag.png
Binary files differ
diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png
new file mode 100644
index 00000000000..1152f1344c7
--- /dev/null
+++ b/app/assets/images/emoji/handball.png
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png
new file mode 100644
index 00000000000..c26cac2df98
--- /dev/null
+++ b/app/assets/images/emoji/handball_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png
new file mode 100644
index 00000000000..7baaf95a9a2
--- /dev/null
+++ b/app/assets/images/emoji/handball_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png
new file mode 100644
index 00000000000..0e3a37c3d40
--- /dev/null
+++ b/app/assets/images/emoji/handball_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png
new file mode 100644
index 00000000000..e1233f38266
--- /dev/null
+++ b/app/assets/images/emoji/handball_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png
new file mode 100644
index 00000000000..6b1eb9b64b0
--- /dev/null
+++ b/app/assets/images/emoji/handball_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png
new file mode 100644
index 00000000000..c5d35fd8138
--- /dev/null
+++ b/app/assets/images/emoji/handshake.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png
new file mode 100644
index 00000000000..8f8fbb9bdca
--- /dev/null
+++ b/app/assets/images/emoji/handshake_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png
new file mode 100644
index 00000000000..336a77a6d78
--- /dev/null
+++ b/app/assets/images/emoji/handshake_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png
new file mode 100644
index 00000000000..95f62d4fecd
--- /dev/null
+++ b/app/assets/images/emoji/handshake_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png
new file mode 100644
index 00000000000..2b0a6433886
--- /dev/null
+++ b/app/assets/images/emoji/handshake_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png
new file mode 100644
index 00000000000..40189ee68e4
--- /dev/null
+++ b/app/assets/images/emoji/handshake_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png
new file mode 100644
index 00000000000..6e26f0070b0
--- /dev/null
+++ b/app/assets/images/emoji/hash.png
Binary files differ
diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png
new file mode 100644
index 00000000000..31dfb511e0e
--- /dev/null
+++ b/app/assets/images/emoji/hatched_chick.png
Binary files differ
diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png
new file mode 100644
index 00000000000..c5b0e8f3bcc
--- /dev/null
+++ b/app/assets/images/emoji/hatching_chick.png
Binary files differ
diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png
new file mode 100644
index 00000000000..0be723085e0
--- /dev/null
+++ b/app/assets/images/emoji/head_bandage.png
Binary files differ
diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png
new file mode 100644
index 00000000000..e9fd34041d8
--- /dev/null
+++ b/app/assets/images/emoji/headphones.png
Binary files differ
diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png
new file mode 100644
index 00000000000..74b6be0c6c5
--- /dev/null
+++ b/app/assets/images/emoji/hear_no_evil.png
Binary files differ
diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png
new file mode 100644
index 00000000000..638cb72dc4e
--- /dev/null
+++ b/app/assets/images/emoji/heart.png
Binary files differ
diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png
new file mode 100644
index 00000000000..5443f60bc63
--- /dev/null
+++ b/app/assets/images/emoji/heart_decoration.png
Binary files differ
diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png
new file mode 100644
index 00000000000..91b520be40b
--- /dev/null
+++ b/app/assets/images/emoji/heart_exclamation.png
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png
new file mode 100644
index 00000000000..73fbee29d4e
--- /dev/null
+++ b/app/assets/images/emoji/heart_eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png
new file mode 100644
index 00000000000..bc5a833f9a1
--- /dev/null
+++ b/app/assets/images/emoji/heart_eyes_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png
new file mode 100644
index 00000000000..0bcf2d1d567
--- /dev/null
+++ b/app/assets/images/emoji/heartbeat.png
Binary files differ
diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png
new file mode 100644
index 00000000000..d6e694e972f
--- /dev/null
+++ b/app/assets/images/emoji/heartpulse.png
Binary files differ
diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png
new file mode 100644
index 00000000000..393c3ed5267
--- /dev/null
+++ b/app/assets/images/emoji/hearts.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png
new file mode 100644
index 00000000000..03bd695377e
--- /dev/null
+++ b/app/assets/images/emoji/heavy_check_mark.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png
new file mode 100644
index 00000000000..df32ab21bea
--- /dev/null
+++ b/app/assets/images/emoji/heavy_division_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png
new file mode 100644
index 00000000000..ef2c2e20590
--- /dev/null
+++ b/app/assets/images/emoji/heavy_dollar_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png
new file mode 100644
index 00000000000..054211caf12
--- /dev/null
+++ b/app/assets/images/emoji/heavy_minus_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png
new file mode 100644
index 00000000000..e47cc1b685d
--- /dev/null
+++ b/app/assets/images/emoji/heavy_multiplication_x.png
Binary files differ
diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png
new file mode 100644
index 00000000000..40799798aaf
--- /dev/null
+++ b/app/assets/images/emoji/heavy_plus_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png
new file mode 100644
index 00000000000..7ec5f39a51a
--- /dev/null
+++ b/app/assets/images/emoji/helicopter.png
Binary files differ
diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png
new file mode 100644
index 00000000000..7140a676038
--- /dev/null
+++ b/app/assets/images/emoji/helmet_with_cross.png
Binary files differ
diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png
new file mode 100644
index 00000000000..d984d1562bb
--- /dev/null
+++ b/app/assets/images/emoji/herb.png
Binary files differ
diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png
new file mode 100644
index 00000000000..39dd3524233
--- /dev/null
+++ b/app/assets/images/emoji/hibiscus.png
Binary files differ
diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png
new file mode 100644
index 00000000000..c41f2d5fd50
--- /dev/null
+++ b/app/assets/images/emoji/high_brightness.png
Binary files differ
diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png
new file mode 100644
index 00000000000..b331cbccc9d
--- /dev/null
+++ b/app/assets/images/emoji/high_heel.png
Binary files differ
diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png
new file mode 100644
index 00000000000..be94e9cbf73
--- /dev/null
+++ b/app/assets/images/emoji/hockey.png
Binary files differ
diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png
new file mode 100644
index 00000000000..517d2ae0deb
--- /dev/null
+++ b/app/assets/images/emoji/hole.png
Binary files differ
diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png
new file mode 100644
index 00000000000..6ab4a2a2651
--- /dev/null
+++ b/app/assets/images/emoji/homes.png
Binary files differ
diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png
new file mode 100644
index 00000000000..9d8f592955e
--- /dev/null
+++ b/app/assets/images/emoji/honey_pot.png
Binary files differ
diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png
new file mode 100644
index 00000000000..7cb1172f4e4
--- /dev/null
+++ b/app/assets/images/emoji/horse.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png
new file mode 100644
index 00000000000..addf9edac56
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png
new file mode 100644
index 00000000000..e9bf4092e98
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png
new file mode 100644
index 00000000000..031bbc3d867
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png
new file mode 100644
index 00000000000..b40ef891f9b
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png
new file mode 100644
index 00000000000..e286cb85065
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png
new file mode 100644
index 00000000000..453c51c6007
--- /dev/null
+++ b/app/assets/images/emoji/horse_racing_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png
new file mode 100644
index 00000000000..1cbce4ae767
--- /dev/null
+++ b/app/assets/images/emoji/hospital.png
Binary files differ
diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png
new file mode 100644
index 00000000000..266675bd577
--- /dev/null
+++ b/app/assets/images/emoji/hot_pepper.png
Binary files differ
diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png
new file mode 100644
index 00000000000..3c3354d94cb
--- /dev/null
+++ b/app/assets/images/emoji/hotdog.png
Binary files differ
diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png
new file mode 100644
index 00000000000..ea8f4c4979a
--- /dev/null
+++ b/app/assets/images/emoji/hotel.png
Binary files differ
diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png
new file mode 100644
index 00000000000..3d9df2d9475
--- /dev/null
+++ b/app/assets/images/emoji/hotsprings.png
Binary files differ
diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png
new file mode 100644
index 00000000000..a5db2d1d3f4
--- /dev/null
+++ b/app/assets/images/emoji/hourglass.png
Binary files differ
diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png
new file mode 100644
index 00000000000..b93b15ed6d8
--- /dev/null
+++ b/app/assets/images/emoji/hourglass_flowing_sand.png
Binary files differ
diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png
new file mode 100644
index 00000000000..01c98a0ba92
--- /dev/null
+++ b/app/assets/images/emoji/house.png
Binary files differ
diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png
new file mode 100644
index 00000000000..c55e81de990
--- /dev/null
+++ b/app/assets/images/emoji/house_abandoned.png
Binary files differ
diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png
new file mode 100644
index 00000000000..0aae41598ef
--- /dev/null
+++ b/app/assets/images/emoji/house_with_garden.png
Binary files differ
diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png
new file mode 100644
index 00000000000..5bba6dc6d51
--- /dev/null
+++ b/app/assets/images/emoji/hugging.png
Binary files differ
diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png
new file mode 100644
index 00000000000..cad0e23132e
--- /dev/null
+++ b/app/assets/images/emoji/hushed.png
Binary files differ
diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png
new file mode 100644
index 00000000000..94267b9c434
--- /dev/null
+++ b/app/assets/images/emoji/ice_cream.png
Binary files differ
diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png
new file mode 100644
index 00000000000..8c449b0c039
--- /dev/null
+++ b/app/assets/images/emoji/ice_skate.png
Binary files differ
diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png
new file mode 100644
index 00000000000..8f6546e31a5
--- /dev/null
+++ b/app/assets/images/emoji/icecream.png
Binary files differ
diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png
new file mode 100644
index 00000000000..5bf69bf7ba8
--- /dev/null
+++ b/app/assets/images/emoji/id.png
Binary files differ
diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png
new file mode 100644
index 00000000000..0c0d589caf0
--- /dev/null
+++ b/app/assets/images/emoji/ideograph_advantage.png
Binary files differ
diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png
new file mode 100644
index 00000000000..9f9a9605539
--- /dev/null
+++ b/app/assets/images/emoji/imp.png
Binary files differ
diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png
new file mode 100644
index 00000000000..41a6be2b0ee
--- /dev/null
+++ b/app/assets/images/emoji/inbox_tray.png
Binary files differ
diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png
new file mode 100644
index 00000000000..fd22e88182e
--- /dev/null
+++ b/app/assets/images/emoji/incoming_envelope.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png
new file mode 100644
index 00000000000..55fc6294d25
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png
new file mode 100644
index 00000000000..3d9e2247940
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png
new file mode 100644
index 00000000000..879e8b7966d
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png
new file mode 100644
index 00000000000..307514eab67
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png
new file mode 100644
index 00000000000..297395dcb3f
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png
new file mode 100644
index 00000000000..26f8f22b28b
--- /dev/null
+++ b/app/assets/images/emoji/information_desk_person_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png
new file mode 100644
index 00000000000..871f2db9314
--- /dev/null
+++ b/app/assets/images/emoji/information_source.png
Binary files differ
diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png
new file mode 100644
index 00000000000..57f5151124f
--- /dev/null
+++ b/app/assets/images/emoji/innocent.png
Binary files differ
diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png
new file mode 100644
index 00000000000..509813e9bb2
--- /dev/null
+++ b/app/assets/images/emoji/interrobang.png
Binary files differ
diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png
new file mode 100644
index 00000000000..fd377acf872
--- /dev/null
+++ b/app/assets/images/emoji/iphone.png
Binary files differ
diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png
new file mode 100644
index 00000000000..7fd834389b7
--- /dev/null
+++ b/app/assets/images/emoji/island.png
Binary files differ
diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png
new file mode 100644
index 00000000000..dfd933f6f36
--- /dev/null
+++ b/app/assets/images/emoji/izakaya_lantern.png
Binary files differ
diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png
new file mode 100644
index 00000000000..44c3fc0aec9
--- /dev/null
+++ b/app/assets/images/emoji/jack_o_lantern.png
Binary files differ
diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png
new file mode 100644
index 00000000000..d86d0a59e12
--- /dev/null
+++ b/app/assets/images/emoji/japan.png
Binary files differ
diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png
new file mode 100644
index 00000000000..64b4e33a1ae
--- /dev/null
+++ b/app/assets/images/emoji/japanese_castle.png
Binary files differ
diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png
new file mode 100644
index 00000000000..515c6a2250e
--- /dev/null
+++ b/app/assets/images/emoji/japanese_goblin.png
Binary files differ
diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png
new file mode 100644
index 00000000000..fe8670fdaf1
--- /dev/null
+++ b/app/assets/images/emoji/japanese_ogre.png
Binary files differ
diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png
new file mode 100644
index 00000000000..2a6869d674c
--- /dev/null
+++ b/app/assets/images/emoji/jeans.png
Binary files differ
diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png
new file mode 100644
index 00000000000..0ba3b1859d8
--- /dev/null
+++ b/app/assets/images/emoji/joy.png
Binary files differ
diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png
new file mode 100644
index 00000000000..aac353179aa
--- /dev/null
+++ b/app/assets/images/emoji/joy_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png
new file mode 100644
index 00000000000..1ee1905434e
--- /dev/null
+++ b/app/assets/images/emoji/joystick.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png
new file mode 100644
index 00000000000..a37f6224a42
--- /dev/null
+++ b/app/assets/images/emoji/juggling.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png
new file mode 100644
index 00000000000..c18eda40031
--- /dev/null
+++ b/app/assets/images/emoji/juggling_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png
new file mode 100644
index 00000000000..de3b7a555b6
--- /dev/null
+++ b/app/assets/images/emoji/juggling_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png
new file mode 100644
index 00000000000..74ab6d85458
--- /dev/null
+++ b/app/assets/images/emoji/juggling_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png
new file mode 100644
index 00000000000..1c57823203f
--- /dev/null
+++ b/app/assets/images/emoji/juggling_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png
new file mode 100644
index 00000000000..c343d6ee98a
--- /dev/null
+++ b/app/assets/images/emoji/juggling_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png
new file mode 100644
index 00000000000..1778c1138e4
--- /dev/null
+++ b/app/assets/images/emoji/kaaba.png
Binary files differ
diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png
new file mode 100644
index 00000000000..319cd1b884c
--- /dev/null
+++ b/app/assets/images/emoji/key.png
Binary files differ
diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png
new file mode 100644
index 00000000000..e11d706c6c8
--- /dev/null
+++ b/app/assets/images/emoji/key2.png
Binary files differ
diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png
new file mode 100644
index 00000000000..75027cb9af7
--- /dev/null
+++ b/app/assets/images/emoji/keyboard.png
Binary files differ
diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png
new file mode 100644
index 00000000000..abe851115d1
--- /dev/null
+++ b/app/assets/images/emoji/kimono.png
Binary files differ
diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png
new file mode 100644
index 00000000000..85e6dcfc4e8
--- /dev/null
+++ b/app/assets/images/emoji/kiss.png
Binary files differ
diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png
new file mode 100644
index 00000000000..a9a0edae17c
--- /dev/null
+++ b/app/assets/images/emoji/kiss_mm.png
Binary files differ
diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png
new file mode 100644
index 00000000000..fdac73cbb1d
--- /dev/null
+++ b/app/assets/images/emoji/kiss_ww.png
Binary files differ
diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png
new file mode 100644
index 00000000000..39d325fd8e3
--- /dev/null
+++ b/app/assets/images/emoji/kissing.png
Binary files differ
diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png
new file mode 100644
index 00000000000..6e0bcc77540
--- /dev/null
+++ b/app/assets/images/emoji/kissing_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png
new file mode 100644
index 00000000000..b684d7d4d6c
--- /dev/null
+++ b/app/assets/images/emoji/kissing_closed_eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png
new file mode 100644
index 00000000000..0ff808fd614
--- /dev/null
+++ b/app/assets/images/emoji/kissing_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png
new file mode 100644
index 00000000000..e181f17099d
--- /dev/null
+++ b/app/assets/images/emoji/kissing_smiling_eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png
new file mode 100644
index 00000000000..dfbd8258074
--- /dev/null
+++ b/app/assets/images/emoji/kiwi.png
Binary files differ
diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png
new file mode 100644
index 00000000000..1acb9f3077b
--- /dev/null
+++ b/app/assets/images/emoji/knife.png
Binary files differ
diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png
new file mode 100644
index 00000000000..a0aa437a98c
--- /dev/null
+++ b/app/assets/images/emoji/koala.png
Binary files differ
diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png
new file mode 100644
index 00000000000..6450eb44d90
--- /dev/null
+++ b/app/assets/images/emoji/koko.png
Binary files differ
diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png
new file mode 100644
index 00000000000..d41c9b4f1e1
--- /dev/null
+++ b/app/assets/images/emoji/label.png
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png
new file mode 100644
index 00000000000..84078ef3127
--- /dev/null
+++ b/app/assets/images/emoji/large_blue_circle.png
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png
new file mode 100644
index 00000000000..416a58bd5a8
--- /dev/null
+++ b/app/assets/images/emoji/large_blue_diamond.png
Binary files differ
diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png
new file mode 100644
index 00000000000..73ff0ac36c8
--- /dev/null
+++ b/app/assets/images/emoji/large_orange_diamond.png
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png
new file mode 100644
index 00000000000..0842a0dd408
--- /dev/null
+++ b/app/assets/images/emoji/last_quarter_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png
new file mode 100644
index 00000000000..94099343c5d
--- /dev/null
+++ b/app/assets/images/emoji/last_quarter_moon_with_face.png
Binary files differ
diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png
new file mode 100644
index 00000000000..d94e9505ba1
--- /dev/null
+++ b/app/assets/images/emoji/laughing.png
Binary files differ
diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png
new file mode 100644
index 00000000000..1e43e1af820
--- /dev/null
+++ b/app/assets/images/emoji/leaves.png
Binary files differ
diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png
new file mode 100644
index 00000000000..13e7561a4bd
--- /dev/null
+++ b/app/assets/images/emoji/ledger.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png
new file mode 100644
index 00000000000..a9d9fd8d59c
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png
new file mode 100644
index 00000000000..1262a6b4b69
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png
new file mode 100644
index 00000000000..40bf70b82b2
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png
new file mode 100644
index 00000000000..93f58145111
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png
new file mode 100644
index 00000000000..d82b5ec91f0
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png
new file mode 100644
index 00000000000..09ae4cd492b
--- /dev/null
+++ b/app/assets/images/emoji/left_facing_fist_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png
new file mode 100644
index 00000000000..887b23f3f25
--- /dev/null
+++ b/app/assets/images/emoji/left_luggage.png
Binary files differ
diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png
new file mode 100644
index 00000000000..7937f24f2ac
--- /dev/null
+++ b/app/assets/images/emoji/left_right_arrow.png
Binary files differ
diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png
new file mode 100644
index 00000000000..ba45c2ad9e9
--- /dev/null
+++ b/app/assets/images/emoji/leftwards_arrow_with_hook.png
Binary files differ
diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png
new file mode 100644
index 00000000000..9a7d95ca220
--- /dev/null
+++ b/app/assets/images/emoji/lemon.png
Binary files differ
diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png
new file mode 100644
index 00000000000..30158d34de9
--- /dev/null
+++ b/app/assets/images/emoji/leo.png
Binary files differ
diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png
new file mode 100644
index 00000000000..8aac3d49448
--- /dev/null
+++ b/app/assets/images/emoji/leopard.png
Binary files differ
diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png
new file mode 100644
index 00000000000..720a3b34119
--- /dev/null
+++ b/app/assets/images/emoji/level_slider.png
Binary files differ
diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png
new file mode 100644
index 00000000000..3dc315a3d91
--- /dev/null
+++ b/app/assets/images/emoji/levitate.png
Binary files differ
diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png
new file mode 100644
index 00000000000..8fd133a357c
--- /dev/null
+++ b/app/assets/images/emoji/libra.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png
new file mode 100644
index 00000000000..afdeaa476af
--- /dev/null
+++ b/app/assets/images/emoji/lifter.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png
new file mode 100644
index 00000000000..febaad123ec
--- /dev/null
+++ b/app/assets/images/emoji/lifter_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png
new file mode 100644
index 00000000000..27ae794a18e
--- /dev/null
+++ b/app/assets/images/emoji/lifter_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png
new file mode 100644
index 00000000000..45c4c22c709
--- /dev/null
+++ b/app/assets/images/emoji/lifter_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png
new file mode 100644
index 00000000000..67dd21d2464
--- /dev/null
+++ b/app/assets/images/emoji/lifter_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png
new file mode 100644
index 00000000000..fa0152038b6
--- /dev/null
+++ b/app/assets/images/emoji/lifter_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png
new file mode 100644
index 00000000000..a64829f5078
--- /dev/null
+++ b/app/assets/images/emoji/light_rail.png
Binary files differ
diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png
new file mode 100644
index 00000000000..ae20f0f8eec
--- /dev/null
+++ b/app/assets/images/emoji/link.png
Binary files differ
diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png
new file mode 100644
index 00000000000..5062ab47ecf
--- /dev/null
+++ b/app/assets/images/emoji/lion_face.png
Binary files differ
diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png
new file mode 100644
index 00000000000..35f3cc2006f
--- /dev/null
+++ b/app/assets/images/emoji/lips.png
Binary files differ
diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png
new file mode 100644
index 00000000000..61a0c084c99
--- /dev/null
+++ b/app/assets/images/emoji/lipstick.png
Binary files differ
diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png
new file mode 100644
index 00000000000..8363876050e
--- /dev/null
+++ b/app/assets/images/emoji/lizard.png
Binary files differ
diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png
new file mode 100644
index 00000000000..5a739c46644
--- /dev/null
+++ b/app/assets/images/emoji/lock.png
Binary files differ
diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png
new file mode 100644
index 00000000000..19a07d162fb
--- /dev/null
+++ b/app/assets/images/emoji/lock_with_ink_pen.png
Binary files differ
diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png
new file mode 100644
index 00000000000..ad76d7bf916
--- /dev/null
+++ b/app/assets/images/emoji/lollipop.png
Binary files differ
diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png
new file mode 100644
index 00000000000..0b82c8fe315
--- /dev/null
+++ b/app/assets/images/emoji/loop.png
Binary files differ
diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png
new file mode 100644
index 00000000000..8370033a539
--- /dev/null
+++ b/app/assets/images/emoji/loud_sound.png
Binary files differ
diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png
new file mode 100644
index 00000000000..5fd76a95b82
--- /dev/null
+++ b/app/assets/images/emoji/loudspeaker.png
Binary files differ
diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png
new file mode 100644
index 00000000000..5e136be6f8b
--- /dev/null
+++ b/app/assets/images/emoji/love_hotel.png
Binary files differ
diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png
new file mode 100644
index 00000000000..3c3c767e784
--- /dev/null
+++ b/app/assets/images/emoji/love_letter.png
Binary files differ
diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png
new file mode 100644
index 00000000000..543011d3961
--- /dev/null
+++ b/app/assets/images/emoji/low_brightness.png
Binary files differ
diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png
new file mode 100644
index 00000000000..02827e2628b
--- /dev/null
+++ b/app/assets/images/emoji/lying_face.png
Binary files differ
diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png
new file mode 100644
index 00000000000..8a3506fc1d7
--- /dev/null
+++ b/app/assets/images/emoji/m.png
Binary files differ
diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png
new file mode 100644
index 00000000000..55487156ac6
--- /dev/null
+++ b/app/assets/images/emoji/mag.png
Binary files differ
diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png
new file mode 100644
index 00000000000..0f4b1bca876
--- /dev/null
+++ b/app/assets/images/emoji/mag_right.png
Binary files differ
diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png
new file mode 100644
index 00000000000..66fd32025b2
--- /dev/null
+++ b/app/assets/images/emoji/mahjong.png
Binary files differ
diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png
new file mode 100644
index 00000000000..ef5174e40dd
--- /dev/null
+++ b/app/assets/images/emoji/mailbox.png
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png
new file mode 100644
index 00000000000..ddc705db0d8
--- /dev/null
+++ b/app/assets/images/emoji/mailbox_closed.png
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png
new file mode 100644
index 00000000000..5460616a5b1
--- /dev/null
+++ b/app/assets/images/emoji/mailbox_with_mail.png
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png
new file mode 100644
index 00000000000..f9aeee6b15a
--- /dev/null
+++ b/app/assets/images/emoji/mailbox_with_no_mail.png
Binary files differ
diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png
new file mode 100644
index 00000000000..857a02e5146
--- /dev/null
+++ b/app/assets/images/emoji/man.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png
new file mode 100644
index 00000000000..ccff3bede5a
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png
new file mode 100644
index 00000000000..e0b9f82d905
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png
new file mode 100644
index 00000000000..a5beed56e2e
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png
new file mode 100644
index 00000000000..2fa20180a6e
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png
new file mode 100644
index 00000000000..bd3528c83ba
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png
new file mode 100644
index 00000000000..41fd4f880c9
--- /dev/null
+++ b/app/assets/images/emoji/man_dancing_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png
new file mode 100644
index 00000000000..5f7e9303f89
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png
new file mode 100644
index 00000000000..7b6b3acd99b
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png
new file mode 100644
index 00000000000..7975191b360
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png
new file mode 100644
index 00000000000..a2816f600ae
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png
new file mode 100644
index 00000000000..ea8291760f9
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png
new file mode 100644
index 00000000000..c743e05fc5e
--- /dev/null
+++ b/app/assets/images/emoji/man_in_tuxedo_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png
new file mode 100644
index 00000000000..bb86e963a80
--- /dev/null
+++ b/app/assets/images/emoji/man_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png
new file mode 100644
index 00000000000..fdeeaff46f5
--- /dev/null
+++ b/app/assets/images/emoji/man_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png
new file mode 100644
index 00000000000..7ae0b5df9cf
--- /dev/null
+++ b/app/assets/images/emoji/man_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png
new file mode 100644
index 00000000000..db14cde99b8
--- /dev/null
+++ b/app/assets/images/emoji/man_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png
new file mode 100644
index 00000000000..7c67a70529c
--- /dev/null
+++ b/app/assets/images/emoji/man_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png
new file mode 100644
index 00000000000..7841e13608d
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
new file mode 100644
index 00000000000..5b7b3def19c
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
new file mode 100644
index 00000000000..c8b9cf87f4b
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
new file mode 100644
index 00000000000..effdd0c4c84
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
new file mode 100644
index 00000000000..f885ff46fa1
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
new file mode 100644
index 00000000000..a6d55ca1380
--- /dev/null
+++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png
new file mode 100644
index 00000000000..51cf047f966
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png
new file mode 100644
index 00000000000..1e12ee4b231
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png
new file mode 100644
index 00000000000..37de4cceb23
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png
new file mode 100644
index 00000000000..f607afd3450
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png
new file mode 100644
index 00000000000..c05695888af
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png
new file mode 100644
index 00000000000..4b4ff64720b
--- /dev/null
+++ b/app/assets/images/emoji/man_with_turban_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png
new file mode 100644
index 00000000000..4bf7541032c
--- /dev/null
+++ b/app/assets/images/emoji/mans_shoe.png
Binary files differ
diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png
new file mode 100644
index 00000000000..15efe32c798
--- /dev/null
+++ b/app/assets/images/emoji/map.png
Binary files differ
diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png
new file mode 100644
index 00000000000..c49acea67f7
--- /dev/null
+++ b/app/assets/images/emoji/maple_leaf.png
Binary files differ
diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png
new file mode 100644
index 00000000000..8d6114761f6
--- /dev/null
+++ b/app/assets/images/emoji/martial_arts_uniform.png
Binary files differ
diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png
new file mode 100644
index 00000000000..1e800acd1c0
--- /dev/null
+++ b/app/assets/images/emoji/mask.png
Binary files differ
diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png
new file mode 100644
index 00000000000..b91d845e374
--- /dev/null
+++ b/app/assets/images/emoji/massage.png
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png
new file mode 100644
index 00000000000..e0f415d3186
--- /dev/null
+++ b/app/assets/images/emoji/massage_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png
new file mode 100644
index 00000000000..0bb244a270b
--- /dev/null
+++ b/app/assets/images/emoji/massage_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png
new file mode 100644
index 00000000000..a117ee81a22
--- /dev/null
+++ b/app/assets/images/emoji/massage_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png
new file mode 100644
index 00000000000..6f42ab017f4
--- /dev/null
+++ b/app/assets/images/emoji/massage_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png
new file mode 100644
index 00000000000..6a388c0d0b5
--- /dev/null
+++ b/app/assets/images/emoji/massage_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png
new file mode 100644
index 00000000000..b20a59d1690
--- /dev/null
+++ b/app/assets/images/emoji/meat_on_bone.png
Binary files differ
diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png
new file mode 100644
index 00000000000..b85896b14da
--- /dev/null
+++ b/app/assets/images/emoji/medal.png
Binary files differ
diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png
new file mode 100644
index 00000000000..4e6735188e3
--- /dev/null
+++ b/app/assets/images/emoji/mega.png
Binary files differ
diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png
new file mode 100644
index 00000000000..c01232d419d
--- /dev/null
+++ b/app/assets/images/emoji/melon.png
Binary files differ
diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png
new file mode 100644
index 00000000000..b4297362869
--- /dev/null
+++ b/app/assets/images/emoji/menorah.png
Binary files differ
diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png
new file mode 100644
index 00000000000..f5a1e1ba0cd
--- /dev/null
+++ b/app/assets/images/emoji/mens.png
Binary files differ
diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png
new file mode 100644
index 00000000000..4aa6e7e0a44
--- /dev/null
+++ b/app/assets/images/emoji/metal.png
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png
new file mode 100644
index 00000000000..c080d2addbd
--- /dev/null
+++ b/app/assets/images/emoji/metal_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png
new file mode 100644
index 00000000000..12313529bcf
--- /dev/null
+++ b/app/assets/images/emoji/metal_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png
new file mode 100644
index 00000000000..ca9be6ae67b
--- /dev/null
+++ b/app/assets/images/emoji/metal_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png
new file mode 100644
index 00000000000..abe28cbf890
--- /dev/null
+++ b/app/assets/images/emoji/metal_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png
new file mode 100644
index 00000000000..0c6b5dd34ed
--- /dev/null
+++ b/app/assets/images/emoji/metal_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png
new file mode 100644
index 00000000000..1de8f0551f3
--- /dev/null
+++ b/app/assets/images/emoji/metro.png
Binary files differ
diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png
new file mode 100644
index 00000000000..d4e6b0def25
--- /dev/null
+++ b/app/assets/images/emoji/microphone.png
Binary files differ
diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png
new file mode 100644
index 00000000000..cd9167654ff
--- /dev/null
+++ b/app/assets/images/emoji/microphone2.png
Binary files differ
diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png
new file mode 100644
index 00000000000..90f5acf6a78
--- /dev/null
+++ b/app/assets/images/emoji/microscope.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png
new file mode 100644
index 00000000000..697f7a25eb2
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png
new file mode 100644
index 00000000000..61ef12a1548
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png
new file mode 100644
index 00000000000..c31a69be9af
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png
new file mode 100644
index 00000000000..73ac216ce63
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png
new file mode 100644
index 00000000000..80b8ab7706d
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png
new file mode 100644
index 00000000000..a8826b196e8
--- /dev/null
+++ b/app/assets/images/emoji/middle_finger_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png
new file mode 100644
index 00000000000..ecd3fb03584
--- /dev/null
+++ b/app/assets/images/emoji/military_medal.png
Binary files differ
diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png
new file mode 100644
index 00000000000..e4fcf2e64f3
--- /dev/null
+++ b/app/assets/images/emoji/milk.png
Binary files differ
diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png
new file mode 100644
index 00000000000..b2b8ac59c5e
--- /dev/null
+++ b/app/assets/images/emoji/milky_way.png
Binary files differ
diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png
new file mode 100644
index 00000000000..c60dd8f47ab
--- /dev/null
+++ b/app/assets/images/emoji/minibus.png
Binary files differ
diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png
new file mode 100644
index 00000000000..9fa94cfbe74
--- /dev/null
+++ b/app/assets/images/emoji/minidisc.png
Binary files differ
diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png
new file mode 100644
index 00000000000..8b661ec1c94
--- /dev/null
+++ b/app/assets/images/emoji/mobile_phone_off.png
Binary files differ
diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png
new file mode 100644
index 00000000000..75fd1e90cb0
--- /dev/null
+++ b/app/assets/images/emoji/money_mouth.png
Binary files differ
diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png
new file mode 100644
index 00000000000..f022b04b3c2
--- /dev/null
+++ b/app/assets/images/emoji/money_with_wings.png
Binary files differ
diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png
new file mode 100644
index 00000000000..b9296be0902
--- /dev/null
+++ b/app/assets/images/emoji/moneybag.png
Binary files differ
diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png
new file mode 100644
index 00000000000..9fae29448e3
--- /dev/null
+++ b/app/assets/images/emoji/monkey.png
Binary files differ
diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png
new file mode 100644
index 00000000000..7cab9b91a82
--- /dev/null
+++ b/app/assets/images/emoji/monkey_face.png
Binary files differ
diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png
new file mode 100644
index 00000000000..11eb1f574bf
--- /dev/null
+++ b/app/assets/images/emoji/monorail.png
Binary files differ
diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png
new file mode 100644
index 00000000000..8b17ddd9d00
--- /dev/null
+++ b/app/assets/images/emoji/mortar_board.png
Binary files differ
diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png
new file mode 100644
index 00000000000..ef770b26d96
--- /dev/null
+++ b/app/assets/images/emoji/mosque.png
Binary files differ
diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png
new file mode 100644
index 00000000000..c5afa72d807
--- /dev/null
+++ b/app/assets/images/emoji/motor_scooter.png
Binary files differ
diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png
new file mode 100644
index 00000000000..0506db1a40f
--- /dev/null
+++ b/app/assets/images/emoji/motorboat.png
Binary files differ
diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png
new file mode 100644
index 00000000000..3d1d567e8ec
--- /dev/null
+++ b/app/assets/images/emoji/motorcycle.png
Binary files differ
diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png
new file mode 100644
index 00000000000..8c3d3d03e3f
--- /dev/null
+++ b/app/assets/images/emoji/motorway.png
Binary files differ
diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png
new file mode 100644
index 00000000000..88a54752458
--- /dev/null
+++ b/app/assets/images/emoji/mount_fuji.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png
new file mode 100644
index 00000000000..6722ebdd294
--- /dev/null
+++ b/app/assets/images/emoji/mountain.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png
new file mode 100644
index 00000000000..41d3dc3ac6f
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png
new file mode 100644
index 00000000000..e9f1daf5e40
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png
new file mode 100644
index 00000000000..555b9e29d4d
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png
new file mode 100644
index 00000000000..7df5508ec8c
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png
new file mode 100644
index 00000000000..f94b3450697
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png
new file mode 100644
index 00000000000..16a45861e1f
--- /dev/null
+++ b/app/assets/images/emoji/mountain_bicyclist_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png
new file mode 100644
index 00000000000..1dea73ca53b
--- /dev/null
+++ b/app/assets/images/emoji/mountain_cableway.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png
new file mode 100644
index 00000000000..ade2218e469
--- /dev/null
+++ b/app/assets/images/emoji/mountain_railway.png
Binary files differ
diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png
new file mode 100644
index 00000000000..76e1cfd8313
--- /dev/null
+++ b/app/assets/images/emoji/mountain_snow.png
Binary files differ
diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png
new file mode 100644
index 00000000000..50afcd3262e
--- /dev/null
+++ b/app/assets/images/emoji/mouse.png
Binary files differ
diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png
new file mode 100644
index 00000000000..20fb041f09f
--- /dev/null
+++ b/app/assets/images/emoji/mouse2.png
Binary files differ
diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png
new file mode 100644
index 00000000000..e84e96ff6e8
--- /dev/null
+++ b/app/assets/images/emoji/mouse_three_button.png
Binary files differ
diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png
new file mode 100644
index 00000000000..4e73b130155
--- /dev/null
+++ b/app/assets/images/emoji/movie_camera.png
Binary files differ
diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png
new file mode 100644
index 00000000000..e6a7779c45b
--- /dev/null
+++ b/app/assets/images/emoji/moyai.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
new file mode 100644
index 00000000000..078f0657f95
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png
new file mode 100644
index 00000000000..d8a695d7035
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png
new file mode 100644
index 00000000000..0e17e8c51f3
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png
new file mode 100644
index 00000000000..c3ee4d1dfae
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png
new file mode 100644
index 00000000000..68a556da2fe
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png
new file mode 100644
index 00000000000..ccab3c40ff2
--- /dev/null
+++ b/app/assets/images/emoji/mrs_claus_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png
new file mode 100644
index 00000000000..7e67c1880f7
--- /dev/null
+++ b/app/assets/images/emoji/muscle.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png
new file mode 100644
index 00000000000..1522942ce51
--- /dev/null
+++ b/app/assets/images/emoji/muscle_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png
new file mode 100644
index 00000000000..569c6e832ca
--- /dev/null
+++ b/app/assets/images/emoji/muscle_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png
new file mode 100644
index 00000000000..0a76b00fa89
--- /dev/null
+++ b/app/assets/images/emoji/muscle_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png
new file mode 100644
index 00000000000..f0cf31328e0
--- /dev/null
+++ b/app/assets/images/emoji/muscle_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png
new file mode 100644
index 00000000000..4fda92460e8
--- /dev/null
+++ b/app/assets/images/emoji/muscle_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png
new file mode 100644
index 00000000000..dd85742ba2c
--- /dev/null
+++ b/app/assets/images/emoji/mushroom.png
Binary files differ
diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png
new file mode 100644
index 00000000000..442b7456842
--- /dev/null
+++ b/app/assets/images/emoji/musical_keyboard.png
Binary files differ
diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png
new file mode 100644
index 00000000000..06691ef61bb
--- /dev/null
+++ b/app/assets/images/emoji/musical_note.png
Binary files differ
diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png
new file mode 100644
index 00000000000..47dc05a8ef5
--- /dev/null
+++ b/app/assets/images/emoji/musical_score.png
Binary files differ
diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png
new file mode 100644
index 00000000000..7c1788e5075
--- /dev/null
+++ b/app/assets/images/emoji/mute.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png
new file mode 100644
index 00000000000..aa52af7050d
--- /dev/null
+++ b/app/assets/images/emoji/nail_care.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png
new file mode 100644
index 00000000000..26e883dd244
--- /dev/null
+++ b/app/assets/images/emoji/nail_care_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png
new file mode 100644
index 00000000000..61257b47ea3
--- /dev/null
+++ b/app/assets/images/emoji/nail_care_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png
new file mode 100644
index 00000000000..29871b05f62
--- /dev/null
+++ b/app/assets/images/emoji/nail_care_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png
new file mode 100644
index 00000000000..2881de0b17d
--- /dev/null
+++ b/app/assets/images/emoji/nail_care_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png
new file mode 100644
index 00000000000..a0b7c0a45a6
--- /dev/null
+++ b/app/assets/images/emoji/nail_care_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png
new file mode 100644
index 00000000000..ec5ee213e20
--- /dev/null
+++ b/app/assets/images/emoji/name_badge.png
Binary files differ
diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png
new file mode 100644
index 00000000000..a566c109c28
--- /dev/null
+++ b/app/assets/images/emoji/nauseated_face.png
Binary files differ
diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png
new file mode 100644
index 00000000000..1804e7f3ff3
--- /dev/null
+++ b/app/assets/images/emoji/necktie.png
Binary files differ
diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png
new file mode 100644
index 00000000000..dae487f1f98
--- /dev/null
+++ b/app/assets/images/emoji/negative_squared_cross_mark.png
Binary files differ
diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png
new file mode 100644
index 00000000000..7820bd581dc
--- /dev/null
+++ b/app/assets/images/emoji/nerd.png
Binary files differ
diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png
new file mode 100644
index 00000000000..065d193afe4
--- /dev/null
+++ b/app/assets/images/emoji/neutral_face.png
Binary files differ
diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png
new file mode 100644
index 00000000000..b4f85488d1a
--- /dev/null
+++ b/app/assets/images/emoji/new.png
Binary files differ
diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png
new file mode 100644
index 00000000000..ecff72caa42
--- /dev/null
+++ b/app/assets/images/emoji/new_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png
new file mode 100644
index 00000000000..150dd12400c
--- /dev/null
+++ b/app/assets/images/emoji/new_moon_with_face.png
Binary files differ
diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png
new file mode 100644
index 00000000000..2aa8f060bde
--- /dev/null
+++ b/app/assets/images/emoji/newspaper.png
Binary files differ
diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png
new file mode 100644
index 00000000000..f64748df2b2
--- /dev/null
+++ b/app/assets/images/emoji/newspaper2.png
Binary files differ
diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png
new file mode 100644
index 00000000000..ee8d20f5ebc
--- /dev/null
+++ b/app/assets/images/emoji/ng.png
Binary files differ
diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png
new file mode 100644
index 00000000000..ca2018f456d
--- /dev/null
+++ b/app/assets/images/emoji/night_with_stars.png
Binary files differ
diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png
new file mode 100644
index 00000000000..9fce3d1eca9
--- /dev/null
+++ b/app/assets/images/emoji/nine.png
Binary files differ
diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png
new file mode 100644
index 00000000000..15cb38dd1e7
--- /dev/null
+++ b/app/assets/images/emoji/no_bell.png
Binary files differ
diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png
new file mode 100644
index 00000000000..19c85421ce9
--- /dev/null
+++ b/app/assets/images/emoji/no_bicycles.png
Binary files differ
diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png
new file mode 100644
index 00000000000..476800fc5c6
--- /dev/null
+++ b/app/assets/images/emoji/no_entry.png
Binary files differ
diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png
new file mode 100644
index 00000000000..d2efd65e74b
--- /dev/null
+++ b/app/assets/images/emoji/no_entry_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png
new file mode 100644
index 00000000000..ed577100322
--- /dev/null
+++ b/app/assets/images/emoji/no_good.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png
new file mode 100644
index 00000000000..5c1a3cbb884
--- /dev/null
+++ b/app/assets/images/emoji/no_good_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png
new file mode 100644
index 00000000000..80d8021f8fe
--- /dev/null
+++ b/app/assets/images/emoji/no_good_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png
new file mode 100644
index 00000000000..635e6a00815
--- /dev/null
+++ b/app/assets/images/emoji/no_good_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png
new file mode 100644
index 00000000000..b96e412a374
--- /dev/null
+++ b/app/assets/images/emoji/no_good_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png
new file mode 100644
index 00000000000..9a7084afa0a
--- /dev/null
+++ b/app/assets/images/emoji/no_good_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png
new file mode 100644
index 00000000000..7b1ae6ea579
--- /dev/null
+++ b/app/assets/images/emoji/no_mobile_phones.png
Binary files differ
diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png
new file mode 100644
index 00000000000..b642f6c1172
--- /dev/null
+++ b/app/assets/images/emoji/no_mouth.png
Binary files differ
diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png
new file mode 100644
index 00000000000..286aa577a23
--- /dev/null
+++ b/app/assets/images/emoji/no_pedestrians.png
Binary files differ
diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png
new file mode 100644
index 00000000000..586b8d29d05
--- /dev/null
+++ b/app/assets/images/emoji/no_smoking.png
Binary files differ
diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png
new file mode 100644
index 00000000000..827d4193f4e
--- /dev/null
+++ b/app/assets/images/emoji/non-potable_water.png
Binary files differ
diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png
new file mode 100644
index 00000000000..2f04ac5f98f
--- /dev/null
+++ b/app/assets/images/emoji/nose.png
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png
new file mode 100644
index 00000000000..8008d17506e
--- /dev/null
+++ b/app/assets/images/emoji/nose_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png
new file mode 100644
index 00000000000..ac17f26e827
--- /dev/null
+++ b/app/assets/images/emoji/nose_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png
new file mode 100644
index 00000000000..d8b6cbe0f8e
--- /dev/null
+++ b/app/assets/images/emoji/nose_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png
new file mode 100644
index 00000000000..004b2631e2e
--- /dev/null
+++ b/app/assets/images/emoji/nose_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png
new file mode 100644
index 00000000000..7b33821f6c9
--- /dev/null
+++ b/app/assets/images/emoji/nose_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png
new file mode 100644
index 00000000000..f6c28b4915d
--- /dev/null
+++ b/app/assets/images/emoji/notebook.png
Binary files differ
diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png
new file mode 100644
index 00000000000..03f566b6d2c
--- /dev/null
+++ b/app/assets/images/emoji/notebook_with_decorative_cover.png
Binary files differ
diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png
new file mode 100644
index 00000000000..85faa10d8ea
--- /dev/null
+++ b/app/assets/images/emoji/notepad_spiral.png
Binary files differ
diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png
new file mode 100644
index 00000000000..57d499aa181
--- /dev/null
+++ b/app/assets/images/emoji/notes.png
Binary files differ
diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png
new file mode 100644
index 00000000000..4b9ae155319
--- /dev/null
+++ b/app/assets/images/emoji/nut_and_bolt.png
Binary files differ
diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png
new file mode 100644
index 00000000000..3fe75ce4675
--- /dev/null
+++ b/app/assets/images/emoji/o.png
Binary files differ
diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png
new file mode 100644
index 00000000000..73278ba194a
--- /dev/null
+++ b/app/assets/images/emoji/o2.png
Binary files differ
diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png
new file mode 100644
index 00000000000..45ff1e87703
--- /dev/null
+++ b/app/assets/images/emoji/ocean.png
Binary files differ
diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png
new file mode 100644
index 00000000000..5ed61004045
--- /dev/null
+++ b/app/assets/images/emoji/octagonal_sign.png
Binary files differ
diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png
new file mode 100644
index 00000000000..72c84074aac
--- /dev/null
+++ b/app/assets/images/emoji/octopus.png
Binary files differ
diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png
new file mode 100644
index 00000000000..d38a849fece
--- /dev/null
+++ b/app/assets/images/emoji/oden.png
Binary files differ
diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png
new file mode 100644
index 00000000000..7eee927d1b0
--- /dev/null
+++ b/app/assets/images/emoji/office.png
Binary files differ
diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png
new file mode 100644
index 00000000000..c4c4d42da8b
--- /dev/null
+++ b/app/assets/images/emoji/oil.png
Binary files differ
diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png
new file mode 100644
index 00000000000..d0d775532ff
--- /dev/null
+++ b/app/assets/images/emoji/ok.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png
new file mode 100644
index 00000000000..028d69b0de3
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png
new file mode 100644
index 00000000000..cecf7b2ab5a
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png
new file mode 100644
index 00000000000..c19239bcd3d
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png
new file mode 100644
index 00000000000..94b65b03ecd
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png
new file mode 100644
index 00000000000..03d26f08e6a
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png
new file mode 100644
index 00000000000..d4b24086364
--- /dev/null
+++ b/app/assets/images/emoji/ok_hand_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png
new file mode 100644
index 00000000000..90a2c7469c4
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png
new file mode 100644
index 00000000000..c99543e785b
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png
new file mode 100644
index 00000000000..ad5fae813db
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png
new file mode 100644
index 00000000000..51bf4fab406
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png
new file mode 100644
index 00000000000..ee3f9dc640a
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png
new file mode 100644
index 00000000000..62a9d9237f7
--- /dev/null
+++ b/app/assets/images/emoji/ok_woman_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png
new file mode 100644
index 00000000000..4ace4e6f308
--- /dev/null
+++ b/app/assets/images/emoji/older_man.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png
new file mode 100644
index 00000000000..ab459baace8
--- /dev/null
+++ b/app/assets/images/emoji/older_man_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png
new file mode 100644
index 00000000000..f4dfc7694ea
--- /dev/null
+++ b/app/assets/images/emoji/older_man_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png
new file mode 100644
index 00000000000..5ffd11792f4
--- /dev/null
+++ b/app/assets/images/emoji/older_man_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png
new file mode 100644
index 00000000000..b350a764bfd
--- /dev/null
+++ b/app/assets/images/emoji/older_man_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png
new file mode 100644
index 00000000000..05fe24a1708
--- /dev/null
+++ b/app/assets/images/emoji/older_man_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png
new file mode 100644
index 00000000000..52dc4987143
--- /dev/null
+++ b/app/assets/images/emoji/older_woman.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png
new file mode 100644
index 00000000000..b49e821402c
--- /dev/null
+++ b/app/assets/images/emoji/older_woman_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png
new file mode 100644
index 00000000000..e86bf5ab3b7
--- /dev/null
+++ b/app/assets/images/emoji/older_woman_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png
new file mode 100644
index 00000000000..83fc14b0874
--- /dev/null
+++ b/app/assets/images/emoji/older_woman_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png
new file mode 100644
index 00000000000..e4aa8a424d4
--- /dev/null
+++ b/app/assets/images/emoji/older_woman_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png
new file mode 100644
index 00000000000..4009012bb0a
--- /dev/null
+++ b/app/assets/images/emoji/older_woman_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png
new file mode 100644
index 00000000000..a35c63c459c
--- /dev/null
+++ b/app/assets/images/emoji/om_symbol.png
Binary files differ
diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png
new file mode 100644
index 00000000000..a0c371ae21e
--- /dev/null
+++ b/app/assets/images/emoji/on.png
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png
new file mode 100644
index 00000000000..3c7e1d52e63
--- /dev/null
+++ b/app/assets/images/emoji/oncoming_automobile.png
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png
new file mode 100644
index 00000000000..ad91e256c7f
--- /dev/null
+++ b/app/assets/images/emoji/oncoming_bus.png
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png
new file mode 100644
index 00000000000..c9109c85b5d
--- /dev/null
+++ b/app/assets/images/emoji/oncoming_police_car.png
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png
new file mode 100644
index 00000000000..fea14e45846
--- /dev/null
+++ b/app/assets/images/emoji/oncoming_taxi.png
Binary files differ
diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png
new file mode 100644
index 00000000000..e6d84b80128
--- /dev/null
+++ b/app/assets/images/emoji/one.png
Binary files differ
diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png
new file mode 100644
index 00000000000..3993b09222f
--- /dev/null
+++ b/app/assets/images/emoji/open_file_folder.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png
new file mode 100644
index 00000000000..1cf75c9101e
--- /dev/null
+++ b/app/assets/images/emoji/open_hands.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png
new file mode 100644
index 00000000000..352d2614f11
--- /dev/null
+++ b/app/assets/images/emoji/open_hands_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png
new file mode 100644
index 00000000000..70824a50c73
--- /dev/null
+++ b/app/assets/images/emoji/open_hands_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png
new file mode 100644
index 00000000000..d7d136bd3db
--- /dev/null
+++ b/app/assets/images/emoji/open_hands_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png
new file mode 100644
index 00000000000..df4eaa711e7
--- /dev/null
+++ b/app/assets/images/emoji/open_hands_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png
new file mode 100644
index 00000000000..7dc04eaebd8
--- /dev/null
+++ b/app/assets/images/emoji/open_hands_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png
new file mode 100644
index 00000000000..a62cd27e148
--- /dev/null
+++ b/app/assets/images/emoji/open_mouth.png
Binary files differ
diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png
new file mode 100644
index 00000000000..0a780a700da
--- /dev/null
+++ b/app/assets/images/emoji/ophiuchus.png
Binary files differ
diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png
new file mode 100644
index 00000000000..ab40e6ae6a2
--- /dev/null
+++ b/app/assets/images/emoji/orange_book.png
Binary files differ
diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png
new file mode 100644
index 00000000000..0530e33a4d4
--- /dev/null
+++ b/app/assets/images/emoji/orthodox_cross.png
Binary files differ
diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png
new file mode 100644
index 00000000000..46493ed5b2c
--- /dev/null
+++ b/app/assets/images/emoji/outbox_tray.png
Binary files differ
diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png
new file mode 100644
index 00000000000..fa6815480c3
--- /dev/null
+++ b/app/assets/images/emoji/owl.png
Binary files differ
diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png
new file mode 100644
index 00000000000..badf5708f2f
--- /dev/null
+++ b/app/assets/images/emoji/ox.png
Binary files differ
diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png
new file mode 100644
index 00000000000..85431756ad8
--- /dev/null
+++ b/app/assets/images/emoji/package.png
Binary files differ
diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png
new file mode 100644
index 00000000000..ba4ed757e01
--- /dev/null
+++ b/app/assets/images/emoji/page_facing_up.png
Binary files differ
diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png
new file mode 100644
index 00000000000..06355319c74
--- /dev/null
+++ b/app/assets/images/emoji/page_with_curl.png
Binary files differ
diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png
new file mode 100644
index 00000000000..b24b99306a2
--- /dev/null
+++ b/app/assets/images/emoji/pager.png
Binary files differ
diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png
new file mode 100644
index 00000000000..28bffbaa3c9
--- /dev/null
+++ b/app/assets/images/emoji/paintbrush.png
Binary files differ
diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png
new file mode 100644
index 00000000000..4bbb10f4f19
--- /dev/null
+++ b/app/assets/images/emoji/palm_tree.png
Binary files differ
diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png
new file mode 100644
index 00000000000..6223d1a28e9
--- /dev/null
+++ b/app/assets/images/emoji/pancakes.png
Binary files differ
diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png
new file mode 100644
index 00000000000..978382775ce
--- /dev/null
+++ b/app/assets/images/emoji/panda_face.png
Binary files differ
diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png
new file mode 100644
index 00000000000..8cd8d4f8750
--- /dev/null
+++ b/app/assets/images/emoji/paperclip.png
Binary files differ
diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png
new file mode 100644
index 00000000000..76021e8c705
--- /dev/null
+++ b/app/assets/images/emoji/paperclips.png
Binary files differ
diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png
new file mode 100644
index 00000000000..63ec7016301
--- /dev/null
+++ b/app/assets/images/emoji/park.png
Binary files differ
diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png
new file mode 100644
index 00000000000..7be7dac27e8
--- /dev/null
+++ b/app/assets/images/emoji/parking.png
Binary files differ
diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png
new file mode 100644
index 00000000000..70453d41528
--- /dev/null
+++ b/app/assets/images/emoji/part_alternation_mark.png
Binary files differ
diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png
new file mode 100644
index 00000000000..a55e59c344c
--- /dev/null
+++ b/app/assets/images/emoji/partly_sunny.png
Binary files differ
diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png
new file mode 100644
index 00000000000..079e34ee4d4
--- /dev/null
+++ b/app/assets/images/emoji/passport_control.png
Binary files differ
diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png
new file mode 100644
index 00000000000..4f07e7ebfd7
--- /dev/null
+++ b/app/assets/images/emoji/pause_button.png
Binary files differ
diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png
new file mode 100644
index 00000000000..86033faf477
--- /dev/null
+++ b/app/assets/images/emoji/peace.png
Binary files differ
diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png
new file mode 100644
index 00000000000..9ab57cbb758
--- /dev/null
+++ b/app/assets/images/emoji/peach.png
Binary files differ
diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png
new file mode 100644
index 00000000000..b64fadad010
--- /dev/null
+++ b/app/assets/images/emoji/peanuts.png
Binary files differ
diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png
new file mode 100644
index 00000000000..3869f718bcf
--- /dev/null
+++ b/app/assets/images/emoji/pear.png
Binary files differ
diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png
new file mode 100644
index 00000000000..6ef7a342433
--- /dev/null
+++ b/app/assets/images/emoji/pen_ballpoint.png
Binary files differ
diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png
new file mode 100644
index 00000000000..3ca4bd2c231
--- /dev/null
+++ b/app/assets/images/emoji/pen_fountain.png
Binary files differ
diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png
new file mode 100644
index 00000000000..edc6155e168
--- /dev/null
+++ b/app/assets/images/emoji/pencil.png
Binary files differ
diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png
new file mode 100644
index 00000000000..3833d590fa2
--- /dev/null
+++ b/app/assets/images/emoji/pencil2.png
Binary files differ
diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png
new file mode 100644
index 00000000000..c0064fb9734
--- /dev/null
+++ b/app/assets/images/emoji/penguin.png
Binary files differ
diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png
new file mode 100644
index 00000000000..490fb566954
--- /dev/null
+++ b/app/assets/images/emoji/pensive.png
Binary files differ
diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png
new file mode 100644
index 00000000000..685441fdaa1
--- /dev/null
+++ b/app/assets/images/emoji/performing_arts.png
Binary files differ
diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png
new file mode 100644
index 00000000000..646a05fe908
--- /dev/null
+++ b/app/assets/images/emoji/persevere.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png
new file mode 100644
index 00000000000..579324959a1
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png
new file mode 100644
index 00000000000..21d3bb43923
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png
new file mode 100644
index 00000000000..973f5fc8382
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png
new file mode 100644
index 00000000000..41fbcc78816
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png
new file mode 100644
index 00000000000..5a37c741030
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png
new file mode 100644
index 00000000000..e08141f3efe
--- /dev/null
+++ b/app/assets/images/emoji/person_frowning_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png
new file mode 100644
index 00000000000..ad6f01a7dda
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png
new file mode 100644
index 00000000000..7d18ef24445
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png
new file mode 100644
index 00000000000..dae1307315c
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png
new file mode 100644
index 00000000000..684677e8e5a
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png
new file mode 100644
index 00000000000..012be0b51f8
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png
new file mode 100644
index 00000000000..d4ecc4cf44b
--- /dev/null
+++ b/app/assets/images/emoji/person_with_blond_hair_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png
new file mode 100644
index 00000000000..10eb0571078
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png
new file mode 100644
index 00000000000..57e826b75a4
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png
new file mode 100644
index 00000000000..3f317c0c25f
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png
new file mode 100644
index 00000000000..d2fbb6c20bf
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png
new file mode 100644
index 00000000000..643ceb4a5c5
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png
new file mode 100644
index 00000000000..b2eb6859c32
--- /dev/null
+++ b/app/assets/images/emoji/person_with_pouting_face_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png
new file mode 100644
index 00000000000..6370fe6d791
--- /dev/null
+++ b/app/assets/images/emoji/pick.png
Binary files differ
diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png
new file mode 100644
index 00000000000..afe05ca1676
--- /dev/null
+++ b/app/assets/images/emoji/pig.png
Binary files differ
diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png
new file mode 100644
index 00000000000..5f31c1a2d75
--- /dev/null
+++ b/app/assets/images/emoji/pig2.png
Binary files differ
diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png
new file mode 100644
index 00000000000..3610ae4a910
--- /dev/null
+++ b/app/assets/images/emoji/pig_nose.png
Binary files differ
diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png
new file mode 100644
index 00000000000..1d4530e77a3
--- /dev/null
+++ b/app/assets/images/emoji/pill.png
Binary files differ
diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png
new file mode 100644
index 00000000000..c89a1606462
--- /dev/null
+++ b/app/assets/images/emoji/pineapple.png
Binary files differ
diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png
new file mode 100644
index 00000000000..ff3c51727d1
--- /dev/null
+++ b/app/assets/images/emoji/ping_pong.png
Binary files differ
diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png
new file mode 100644
index 00000000000..7f6f646a95c
--- /dev/null
+++ b/app/assets/images/emoji/pisces.png
Binary files differ
diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png
new file mode 100644
index 00000000000..e07365cb398
--- /dev/null
+++ b/app/assets/images/emoji/pizza.png
Binary files differ
diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png
new file mode 100644
index 00000000000..207d59cce85
--- /dev/null
+++ b/app/assets/images/emoji/place_of_worship.png
Binary files differ
diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png
new file mode 100644
index 00000000000..a9f857139ac
--- /dev/null
+++ b/app/assets/images/emoji/play_pause.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png
new file mode 100644
index 00000000000..00d3d13ab5c
--- /dev/null
+++ b/app/assets/images/emoji/point_down.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png
new file mode 100644
index 00000000000..140f157d8c7
--- /dev/null
+++ b/app/assets/images/emoji/point_down_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png
new file mode 100644
index 00000000000..d518544f7fa
--- /dev/null
+++ b/app/assets/images/emoji/point_down_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png
new file mode 100644
index 00000000000..018b688b8b7
--- /dev/null
+++ b/app/assets/images/emoji/point_down_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png
new file mode 100644
index 00000000000..98845bf6f72
--- /dev/null
+++ b/app/assets/images/emoji/point_down_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png
new file mode 100644
index 00000000000..9a9b039a9fc
--- /dev/null
+++ b/app/assets/images/emoji/point_down_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png
new file mode 100644
index 00000000000..599fa2e3cf1
--- /dev/null
+++ b/app/assets/images/emoji/point_left.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png
new file mode 100644
index 00000000000..88e2c306076
--- /dev/null
+++ b/app/assets/images/emoji/point_left_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png
new file mode 100644
index 00000000000..d3c89d87c5f
--- /dev/null
+++ b/app/assets/images/emoji/point_left_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png
new file mode 100644
index 00000000000..b23b9167358
--- /dev/null
+++ b/app/assets/images/emoji/point_left_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png
new file mode 100644
index 00000000000..3093f325c27
--- /dev/null
+++ b/app/assets/images/emoji/point_left_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png
new file mode 100644
index 00000000000..2b4cbfa120c
--- /dev/null
+++ b/app/assets/images/emoji/point_left_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png
new file mode 100644
index 00000000000..93a3cd34aa5
--- /dev/null
+++ b/app/assets/images/emoji/point_right.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png
new file mode 100644
index 00000000000..4a28c6bbc89
--- /dev/null
+++ b/app/assets/images/emoji/point_right_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png
new file mode 100644
index 00000000000..7cb13231733
--- /dev/null
+++ b/app/assets/images/emoji/point_right_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png
new file mode 100644
index 00000000000..5514807d71a
--- /dev/null
+++ b/app/assets/images/emoji/point_right_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png
new file mode 100644
index 00000000000..b8541d6440d
--- /dev/null
+++ b/app/assets/images/emoji/point_right_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png
new file mode 100644
index 00000000000..1b7aab07bb1
--- /dev/null
+++ b/app/assets/images/emoji/point_right_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png
new file mode 100644
index 00000000000..f4978ff0f00
--- /dev/null
+++ b/app/assets/images/emoji/point_up.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png
new file mode 100644
index 00000000000..bc496dfeae4
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png
new file mode 100644
index 00000000000..a12a7e78430
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png
new file mode 100644
index 00000000000..cdff40ceab0
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png
new file mode 100644
index 00000000000..a07ce9e5ae8
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png
new file mode 100644
index 00000000000..4f86c88ba42
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png
new file mode 100644
index 00000000000..ed1b26c35d3
--- /dev/null
+++ b/app/assets/images/emoji/point_up_2_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png
new file mode 100644
index 00000000000..6a9db21d64c
--- /dev/null
+++ b/app/assets/images/emoji/point_up_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png
new file mode 100644
index 00000000000..15aa9ea0e05
--- /dev/null
+++ b/app/assets/images/emoji/point_up_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png
new file mode 100644
index 00000000000..652b73a9c5d
--- /dev/null
+++ b/app/assets/images/emoji/point_up_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png
new file mode 100644
index 00000000000..692bad926e9
--- /dev/null
+++ b/app/assets/images/emoji/point_up_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png
new file mode 100644
index 00000000000..1e1b10fb71c
--- /dev/null
+++ b/app/assets/images/emoji/point_up_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png
new file mode 100644
index 00000000000..3da4253de7e
--- /dev/null
+++ b/app/assets/images/emoji/police_car.png
Binary files differ
diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png
new file mode 100644
index 00000000000..8ec39e396af
--- /dev/null
+++ b/app/assets/images/emoji/poodle.png
Binary files differ
diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png
new file mode 100644
index 00000000000..10b15e72d56
--- /dev/null
+++ b/app/assets/images/emoji/poop.png
Binary files differ
diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png
new file mode 100644
index 00000000000..36853e381d4
--- /dev/null
+++ b/app/assets/images/emoji/popcorn.png
Binary files differ
diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png
new file mode 100644
index 00000000000..a23848f9aa0
--- /dev/null
+++ b/app/assets/images/emoji/post_office.png
Binary files differ
diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png
new file mode 100644
index 00000000000..c173b8dbd67
--- /dev/null
+++ b/app/assets/images/emoji/postal_horn.png
Binary files differ
diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png
new file mode 100644
index 00000000000..07c9c4ab3d6
--- /dev/null
+++ b/app/assets/images/emoji/postbox.png
Binary files differ
diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png
new file mode 100644
index 00000000000..2c610049459
--- /dev/null
+++ b/app/assets/images/emoji/potable_water.png
Binary files differ
diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png
new file mode 100644
index 00000000000..70350ca2c0a
--- /dev/null
+++ b/app/assets/images/emoji/potato.png
Binary files differ
diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png
new file mode 100644
index 00000000000..8795c6c66ff
--- /dev/null
+++ b/app/assets/images/emoji/pouch.png
Binary files differ
diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png
new file mode 100644
index 00000000000..eea4a53a2f9
--- /dev/null
+++ b/app/assets/images/emoji/poultry_leg.png
Binary files differ
diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png
new file mode 100644
index 00000000000..a0d4c4099e9
--- /dev/null
+++ b/app/assets/images/emoji/pound.png
Binary files differ
diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png
new file mode 100644
index 00000000000..41ddfeab42b
--- /dev/null
+++ b/app/assets/images/emoji/pouting_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png
new file mode 100644
index 00000000000..8347f2435be
--- /dev/null
+++ b/app/assets/images/emoji/pray.png
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png
new file mode 100644
index 00000000000..060ef257172
--- /dev/null
+++ b/app/assets/images/emoji/pray_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png
new file mode 100644
index 00000000000..56dc607c07a
--- /dev/null
+++ b/app/assets/images/emoji/pray_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png
new file mode 100644
index 00000000000..0f33b862008
--- /dev/null
+++ b/app/assets/images/emoji/pray_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png
new file mode 100644
index 00000000000..2ea8dc11657
--- /dev/null
+++ b/app/assets/images/emoji/pray_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png
new file mode 100644
index 00000000000..2128a6c4703
--- /dev/null
+++ b/app/assets/images/emoji/pray_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png
new file mode 100644
index 00000000000..a4b6dfcc62e
--- /dev/null
+++ b/app/assets/images/emoji/prayer_beads.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png
new file mode 100644
index 00000000000..084e83a414a
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png
new file mode 100644
index 00000000000..a78703b33aa
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png
new file mode 100644
index 00000000000..0068c6c4a77
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png
new file mode 100644
index 00000000000..3206296b684
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png
new file mode 100644
index 00000000000..120fda5cd8c
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png
new file mode 100644
index 00000000000..569bfdf05ce
--- /dev/null
+++ b/app/assets/images/emoji/pregnant_woman_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png
new file mode 100644
index 00000000000..38d69344c84
--- /dev/null
+++ b/app/assets/images/emoji/prince.png
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png
new file mode 100644
index 00000000000..849930c8887
--- /dev/null
+++ b/app/assets/images/emoji/prince_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png
new file mode 100644
index 00000000000..23d8b3b1285
--- /dev/null
+++ b/app/assets/images/emoji/prince_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png
new file mode 100644
index 00000000000..db6dfff0647
--- /dev/null
+++ b/app/assets/images/emoji/prince_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png
new file mode 100644
index 00000000000..8e10f8be6a8
--- /dev/null
+++ b/app/assets/images/emoji/prince_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png
new file mode 100644
index 00000000000..138d4ea7048
--- /dev/null
+++ b/app/assets/images/emoji/prince_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png
new file mode 100644
index 00000000000..879e9fa8c5d
--- /dev/null
+++ b/app/assets/images/emoji/princess.png
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png
new file mode 100644
index 00000000000..c28078cdc36
--- /dev/null
+++ b/app/assets/images/emoji/princess_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png
new file mode 100644
index 00000000000..dcd20e6ecd4
--- /dev/null
+++ b/app/assets/images/emoji/princess_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png
new file mode 100644
index 00000000000..cde6f315c56
--- /dev/null
+++ b/app/assets/images/emoji/princess_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png
new file mode 100644
index 00000000000..c71e69caaef
--- /dev/null
+++ b/app/assets/images/emoji/princess_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png
new file mode 100644
index 00000000000..063e2645910
--- /dev/null
+++ b/app/assets/images/emoji/princess_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png
new file mode 100644
index 00000000000..027c830f0fe
--- /dev/null
+++ b/app/assets/images/emoji/printer.png
Binary files differ
diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png
new file mode 100644
index 00000000000..ce9ab0daa28
--- /dev/null
+++ b/app/assets/images/emoji/projector.png
Binary files differ
diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png
new file mode 100644
index 00000000000..b14ca5f5211
--- /dev/null
+++ b/app/assets/images/emoji/punch.png
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png
new file mode 100644
index 00000000000..93c7d17fb47
--- /dev/null
+++ b/app/assets/images/emoji/punch_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png
new file mode 100644
index 00000000000..c0a1af6e10a
--- /dev/null
+++ b/app/assets/images/emoji/punch_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png
new file mode 100644
index 00000000000..1458b021201
--- /dev/null
+++ b/app/assets/images/emoji/punch_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png
new file mode 100644
index 00000000000..c1466bfcdef
--- /dev/null
+++ b/app/assets/images/emoji/punch_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png
new file mode 100644
index 00000000000..00b4ddb8953
--- /dev/null
+++ b/app/assets/images/emoji/punch_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png
new file mode 100644
index 00000000000..95c53a9ade6
--- /dev/null
+++ b/app/assets/images/emoji/purple_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png
new file mode 100644
index 00000000000..981346193c5
--- /dev/null
+++ b/app/assets/images/emoji/purse.png
Binary files differ
diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png
new file mode 100644
index 00000000000..57e07d7f4cc
--- /dev/null
+++ b/app/assets/images/emoji/pushpin.png
Binary files differ
diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png
new file mode 100644
index 00000000000..82a84f9a375
--- /dev/null
+++ b/app/assets/images/emoji/put_litter_in_its_place.png
Binary files differ
diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png
new file mode 100644
index 00000000000..5a58f3458aa
--- /dev/null
+++ b/app/assets/images/emoji/question.png
Binary files differ
diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png
new file mode 100644
index 00000000000..ea75ab0426e
--- /dev/null
+++ b/app/assets/images/emoji/rabbit.png
Binary files differ
diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png
new file mode 100644
index 00000000000..2c8a29c642f
--- /dev/null
+++ b/app/assets/images/emoji/rabbit2.png
Binary files differ
diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png
new file mode 100644
index 00000000000..fe3f045f446
--- /dev/null
+++ b/app/assets/images/emoji/race_car.png
Binary files differ
diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png
new file mode 100644
index 00000000000..b3e73cc8903
--- /dev/null
+++ b/app/assets/images/emoji/racehorse.png
Binary files differ
diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png
new file mode 100644
index 00000000000..dec381fa242
--- /dev/null
+++ b/app/assets/images/emoji/radio.png
Binary files differ
diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png
new file mode 100644
index 00000000000..3a23449d917
--- /dev/null
+++ b/app/assets/images/emoji/radio_button.png
Binary files differ
diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png
new file mode 100644
index 00000000000..3b46199fe37
--- /dev/null
+++ b/app/assets/images/emoji/radioactive.png
Binary files differ
diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png
new file mode 100644
index 00000000000..9d739bd40ad
--- /dev/null
+++ b/app/assets/images/emoji/rage.png
Binary files differ
diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png
new file mode 100644
index 00000000000..a9acbf13008
--- /dev/null
+++ b/app/assets/images/emoji/railway_car.png
Binary files differ
diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png
new file mode 100644
index 00000000000..e1a7a0d1430
--- /dev/null
+++ b/app/assets/images/emoji/railway_track.png
Binary files differ
diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png
new file mode 100644
index 00000000000..154735d7147
--- /dev/null
+++ b/app/assets/images/emoji/rainbow.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png
new file mode 100644
index 00000000000..479234294b4
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png
new file mode 100644
index 00000000000..813d28499b5
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png
new file mode 100644
index 00000000000..192ff795e37
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png
new file mode 100644
index 00000000000..61a727abe6b
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png
new file mode 100644
index 00000000000..2e83da511f5
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png
new file mode 100644
index 00000000000..d7a5b95a02c
--- /dev/null
+++ b/app/assets/images/emoji/raised_back_of_hand_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png
new file mode 100644
index 00000000000..6b2954315d1
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png
new file mode 100644
index 00000000000..3b752902c07
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png
new file mode 100644
index 00000000000..44e2a514c60
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png
new file mode 100644
index 00000000000..5bb62a7528a
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png
new file mode 100644
index 00000000000..c7f8c9ec270
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png
new file mode 100644
index 00000000000..c601b58a73e
--- /dev/null
+++ b/app/assets/images/emoji/raised_hand_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png
new file mode 100644
index 00000000000..c0155f728e7
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png
new file mode 100644
index 00000000000..1168b8236b6
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png
new file mode 100644
index 00000000000..322de622903
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png
new file mode 100644
index 00000000000..2aa24e05ae1
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png
new file mode 100644
index 00000000000..f31bf0db992
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png
new file mode 100644
index 00000000000..5e95067f98b
--- /dev/null
+++ b/app/assets/images/emoji/raised_hands_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png
new file mode 100644
index 00000000000..2880708c0cc
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png
new file mode 100644
index 00000000000..1c90e3e2689
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png
new file mode 100644
index 00000000000..82c3ef2bfc5
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png
new file mode 100644
index 00000000000..1b1da2aa0ca
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png
new file mode 100644
index 00000000000..e453855c01f
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png
new file mode 100644
index 00000000000..b86200fd844
--- /dev/null
+++ b/app/assets/images/emoji/raising_hand_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png
new file mode 100644
index 00000000000..52a44464c9b
--- /dev/null
+++ b/app/assets/images/emoji/ram.png
Binary files differ
diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png
new file mode 100644
index 00000000000..c1cb7cd7384
--- /dev/null
+++ b/app/assets/images/emoji/ramen.png
Binary files differ
diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png
new file mode 100644
index 00000000000..86219144f10
--- /dev/null
+++ b/app/assets/images/emoji/rat.png
Binary files differ
diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png
new file mode 100644
index 00000000000..ada52830fce
--- /dev/null
+++ b/app/assets/images/emoji/record_button.png
Binary files differ
diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png
new file mode 100644
index 00000000000..9221f095c37
--- /dev/null
+++ b/app/assets/images/emoji/recycle.png
Binary files differ
diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png
new file mode 100644
index 00000000000..b3e6a774dea
--- /dev/null
+++ b/app/assets/images/emoji/red_car.png
Binary files differ
diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png
new file mode 100644
index 00000000000..4bef930d92f
--- /dev/null
+++ b/app/assets/images/emoji/red_circle.png
Binary files differ
diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png
new file mode 100644
index 00000000000..53ef9f2d4e6
--- /dev/null
+++ b/app/assets/images/emoji/registered.png
Binary files differ
diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png
new file mode 100644
index 00000000000..e9e53c03d45
--- /dev/null
+++ b/app/assets/images/emoji/relaxed.png
Binary files differ
diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png
new file mode 100644
index 00000000000..715ad0bf53f
--- /dev/null
+++ b/app/assets/images/emoji/relieved.png
Binary files differ
diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png
new file mode 100644
index 00000000000..3988bbd094c
--- /dev/null
+++ b/app/assets/images/emoji/reminder_ribbon.png
Binary files differ
diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png
new file mode 100644
index 00000000000..540ce4e0fba
--- /dev/null
+++ b/app/assets/images/emoji/repeat.png
Binary files differ
diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png
new file mode 100644
index 00000000000..9567e83337f
--- /dev/null
+++ b/app/assets/images/emoji/repeat_one.png
Binary files differ
diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png
new file mode 100644
index 00000000000..9588e0f0ef7
--- /dev/null
+++ b/app/assets/images/emoji/restroom.png
Binary files differ
diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png
new file mode 100644
index 00000000000..7b9d1948f73
--- /dev/null
+++ b/app/assets/images/emoji/revolving_hearts.png
Binary files differ
diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png
new file mode 100644
index 00000000000..e22e2bd3da5
--- /dev/null
+++ b/app/assets/images/emoji/rewind.png
Binary files differ
diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png
new file mode 100644
index 00000000000..12f4e0d9d9b
--- /dev/null
+++ b/app/assets/images/emoji/rhino.png
Binary files differ
diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png
new file mode 100644
index 00000000000..0f253c3d8c8
--- /dev/null
+++ b/app/assets/images/emoji/ribbon.png
Binary files differ
diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png
new file mode 100644
index 00000000000..6e3ac7956b1
--- /dev/null
+++ b/app/assets/images/emoji/rice.png
Binary files differ
diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png
new file mode 100644
index 00000000000..d3d8ee25cb8
--- /dev/null
+++ b/app/assets/images/emoji/rice_ball.png
Binary files differ
diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png
new file mode 100644
index 00000000000..7fbd08e4ff9
--- /dev/null
+++ b/app/assets/images/emoji/rice_cracker.png
Binary files differ
diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png
new file mode 100644
index 00000000000..1a28426592a
--- /dev/null
+++ b/app/assets/images/emoji/rice_scene.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png
new file mode 100644
index 00000000000..754ed066d2c
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png
new file mode 100644
index 00000000000..33ded2f61a6
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png
new file mode 100644
index 00000000000..88054e335c7
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png
new file mode 100644
index 00000000000..84b9f5da7f7
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png
new file mode 100644
index 00000000000..e741cfea68b
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png
new file mode 100644
index 00000000000..cf66d760c1f
--- /dev/null
+++ b/app/assets/images/emoji/right_facing_fist_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png
new file mode 100644
index 00000000000..87d227adb74
--- /dev/null
+++ b/app/assets/images/emoji/ring.png
Binary files differ
diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png
new file mode 100644
index 00000000000..7cc62612c6a
--- /dev/null
+++ b/app/assets/images/emoji/robot.png
Binary files differ
diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png
new file mode 100644
index 00000000000..0d8da089a37
--- /dev/null
+++ b/app/assets/images/emoji/rocket.png
Binary files differ
diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png
new file mode 100644
index 00000000000..b1736fedfeb
--- /dev/null
+++ b/app/assets/images/emoji/rofl.png
Binary files differ
diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png
new file mode 100644
index 00000000000..5b849e071e8
--- /dev/null
+++ b/app/assets/images/emoji/roller_coaster.png
Binary files differ
diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png
new file mode 100644
index 00000000000..2f77b9fc3b9
--- /dev/null
+++ b/app/assets/images/emoji/rolling_eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png
new file mode 100644
index 00000000000..bbf2bbff97a
--- /dev/null
+++ b/app/assets/images/emoji/rooster.png
Binary files differ
diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png
new file mode 100644
index 00000000000..52c286d31ce
--- /dev/null
+++ b/app/assets/images/emoji/rose.png
Binary files differ
diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png
new file mode 100644
index 00000000000..8030e494bcf
--- /dev/null
+++ b/app/assets/images/emoji/rosette.png
Binary files differ
diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png
new file mode 100644
index 00000000000..cad66b0afef
--- /dev/null
+++ b/app/assets/images/emoji/rotating_light.png
Binary files differ
diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png
new file mode 100644
index 00000000000..28b9d72866e
--- /dev/null
+++ b/app/assets/images/emoji/round_pushpin.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png
new file mode 100644
index 00000000000..dd4dfc095d9
--- /dev/null
+++ b/app/assets/images/emoji/rowboat.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png
new file mode 100644
index 00000000000..5e5d18548cb
--- /dev/null
+++ b/app/assets/images/emoji/rowboat_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png
new file mode 100644
index 00000000000..9b123ef8871
--- /dev/null
+++ b/app/assets/images/emoji/rowboat_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png
new file mode 100644
index 00000000000..8ebd89a55f5
--- /dev/null
+++ b/app/assets/images/emoji/rowboat_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png
new file mode 100644
index 00000000000..2b0d04f8725
--- /dev/null
+++ b/app/assets/images/emoji/rowboat_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png
new file mode 100644
index 00000000000..b346f2dfc84
--- /dev/null
+++ b/app/assets/images/emoji/rowboat_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png
new file mode 100644
index 00000000000..b1872273436
--- /dev/null
+++ b/app/assets/images/emoji/rugby_football.png
Binary files differ
diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png
new file mode 100644
index 00000000000..e914915976a
--- /dev/null
+++ b/app/assets/images/emoji/runner.png
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png
new file mode 100644
index 00000000000..9355239a52d
--- /dev/null
+++ b/app/assets/images/emoji/runner_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png
new file mode 100644
index 00000000000..6112fd5c376
--- /dev/null
+++ b/app/assets/images/emoji/runner_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png
new file mode 100644
index 00000000000..625ec708f48
--- /dev/null
+++ b/app/assets/images/emoji/runner_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png
new file mode 100644
index 00000000000..242f1b56337
--- /dev/null
+++ b/app/assets/images/emoji/runner_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png
new file mode 100644
index 00000000000..2976c6f019f
--- /dev/null
+++ b/app/assets/images/emoji/runner_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png
new file mode 100644
index 00000000000..6d83c06b803
--- /dev/null
+++ b/app/assets/images/emoji/running_shirt_with_sash.png
Binary files differ
diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png
new file mode 100644
index 00000000000..900f9633247
--- /dev/null
+++ b/app/assets/images/emoji/sa.png
Binary files differ
diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png
new file mode 100644
index 00000000000..f8d94ff2923
--- /dev/null
+++ b/app/assets/images/emoji/sagittarius.png
Binary files differ
diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png
new file mode 100644
index 00000000000..772ef11da5d
--- /dev/null
+++ b/app/assets/images/emoji/sailboat.png
Binary files differ
diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png
new file mode 100644
index 00000000000..2933f5672c4
--- /dev/null
+++ b/app/assets/images/emoji/sake.png
Binary files differ
diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png
new file mode 100644
index 00000000000..c89f9341158
--- /dev/null
+++ b/app/assets/images/emoji/salad.png
Binary files differ
diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png
new file mode 100644
index 00000000000..9d9f5122b7a
--- /dev/null
+++ b/app/assets/images/emoji/sandal.png
Binary files differ
diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png
new file mode 100644
index 00000000000..bc83ab80d52
--- /dev/null
+++ b/app/assets/images/emoji/santa.png
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png
new file mode 100644
index 00000000000..5233ffb7174
--- /dev/null
+++ b/app/assets/images/emoji/santa_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png
new file mode 100644
index 00000000000..4e845438197
--- /dev/null
+++ b/app/assets/images/emoji/santa_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png
new file mode 100644
index 00000000000..7fc4f33b60f
--- /dev/null
+++ b/app/assets/images/emoji/santa_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png
new file mode 100644
index 00000000000..d1d5a15132d
--- /dev/null
+++ b/app/assets/images/emoji/santa_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png
new file mode 100644
index 00000000000..4d697a01f24
--- /dev/null
+++ b/app/assets/images/emoji/santa_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png
new file mode 100644
index 00000000000..db0372795f4
--- /dev/null
+++ b/app/assets/images/emoji/satellite.png
Binary files differ
diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png
new file mode 100644
index 00000000000..4ba55d6e297
--- /dev/null
+++ b/app/assets/images/emoji/satellite_orbital.png
Binary files differ
diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png
new file mode 100644
index 00000000000..a392faec291
--- /dev/null
+++ b/app/assets/images/emoji/saxophone.png
Binary files differ
diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png
new file mode 100644
index 00000000000..0757eda1684
--- /dev/null
+++ b/app/assets/images/emoji/scales.png
Binary files differ
diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png
new file mode 100644
index 00000000000..269759534f0
--- /dev/null
+++ b/app/assets/images/emoji/school.png
Binary files differ
diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png
new file mode 100644
index 00000000000..9997c86e7dc
--- /dev/null
+++ b/app/assets/images/emoji/school_satchel.png
Binary files differ
diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png
new file mode 100644
index 00000000000..270571c8cdd
--- /dev/null
+++ b/app/assets/images/emoji/scissors.png
Binary files differ
diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png
new file mode 100644
index 00000000000..4ab7ef59cd2
--- /dev/null
+++ b/app/assets/images/emoji/scooter.png
Binary files differ
diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png
new file mode 100644
index 00000000000..449a6b281c9
--- /dev/null
+++ b/app/assets/images/emoji/scorpion.png
Binary files differ
diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png
new file mode 100644
index 00000000000..c31a9920455
--- /dev/null
+++ b/app/assets/images/emoji/scorpius.png
Binary files differ
diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png
new file mode 100644
index 00000000000..c3bea9f2510
--- /dev/null
+++ b/app/assets/images/emoji/scream.png
Binary files differ
diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png
new file mode 100644
index 00000000000..15803ad8e6e
--- /dev/null
+++ b/app/assets/images/emoji/scream_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png
new file mode 100644
index 00000000000..50ee5dcd4b9
--- /dev/null
+++ b/app/assets/images/emoji/scroll.png
Binary files differ
diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png
new file mode 100644
index 00000000000..a6d72d95adb
--- /dev/null
+++ b/app/assets/images/emoji/seat.png
Binary files differ
diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png
new file mode 100644
index 00000000000..17b011268b6
--- /dev/null
+++ b/app/assets/images/emoji/second_place.png
Binary files differ
diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png
new file mode 100644
index 00000000000..5fd72608e60
--- /dev/null
+++ b/app/assets/images/emoji/secret.png
Binary files differ
diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png
new file mode 100644
index 00000000000..5187e474531
--- /dev/null
+++ b/app/assets/images/emoji/see_no_evil.png
Binary files differ
diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png
new file mode 100644
index 00000000000..ae0948bcfd6
--- /dev/null
+++ b/app/assets/images/emoji/seedling.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png
new file mode 100644
index 00000000000..6a1ba75c7e3
--- /dev/null
+++ b/app/assets/images/emoji/selfie.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png
new file mode 100644
index 00000000000..290e075b56f
--- /dev/null
+++ b/app/assets/images/emoji/selfie_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png
new file mode 100644
index 00000000000..fcd9595b643
--- /dev/null
+++ b/app/assets/images/emoji/selfie_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png
new file mode 100644
index 00000000000..f3a22fdf435
--- /dev/null
+++ b/app/assets/images/emoji/selfie_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png
new file mode 100644
index 00000000000..cdecf6d9f4e
--- /dev/null
+++ b/app/assets/images/emoji/selfie_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png
new file mode 100644
index 00000000000..86acbb6c202
--- /dev/null
+++ b/app/assets/images/emoji/selfie_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png
new file mode 100644
index 00000000000..9b3476ae7c7
--- /dev/null
+++ b/app/assets/images/emoji/seven.png
Binary files differ
diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png
new file mode 100644
index 00000000000..663a1006acd
--- /dev/null
+++ b/app/assets/images/emoji/shallow_pan_of_food.png
Binary files differ
diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png
new file mode 100644
index 00000000000..f202aecfe6f
--- /dev/null
+++ b/app/assets/images/emoji/shamrock.png
Binary files differ
diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png
new file mode 100644
index 00000000000..c75076d57d8
--- /dev/null
+++ b/app/assets/images/emoji/shark.png
Binary files differ
diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png
new file mode 100644
index 00000000000..36dfb53ca93
--- /dev/null
+++ b/app/assets/images/emoji/shaved_ice.png
Binary files differ
diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png
new file mode 100644
index 00000000000..102b8a52b28
--- /dev/null
+++ b/app/assets/images/emoji/sheep.png
Binary files differ
diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png
new file mode 100644
index 00000000000..55721629f62
--- /dev/null
+++ b/app/assets/images/emoji/shell.png
Binary files differ
diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png
new file mode 100644
index 00000000000..610bf033ce0
--- /dev/null
+++ b/app/assets/images/emoji/shield.png
Binary files differ
diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png
new file mode 100644
index 00000000000..5a344975bf3
--- /dev/null
+++ b/app/assets/images/emoji/shinto_shrine.png
Binary files differ
diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png
new file mode 100644
index 00000000000..62d54f7d6c9
--- /dev/null
+++ b/app/assets/images/emoji/ship.png
Binary files differ
diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png
new file mode 100644
index 00000000000..af08dec8b59
--- /dev/null
+++ b/app/assets/images/emoji/shirt.png
Binary files differ
diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png
new file mode 100644
index 00000000000..99f2a2b13ac
--- /dev/null
+++ b/app/assets/images/emoji/shopping_bags.png
Binary files differ
diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png
new file mode 100644
index 00000000000..1086fe6e456
--- /dev/null
+++ b/app/assets/images/emoji/shopping_cart.png
Binary files differ
diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png
new file mode 100644
index 00000000000..156776a2e52
--- /dev/null
+++ b/app/assets/images/emoji/shower.png
Binary files differ
diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png
new file mode 100644
index 00000000000..49eff28a71e
--- /dev/null
+++ b/app/assets/images/emoji/shrimp.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png
new file mode 100644
index 00000000000..76e63bfac77
--- /dev/null
+++ b/app/assets/images/emoji/shrug.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png
new file mode 100644
index 00000000000..1c895e64468
--- /dev/null
+++ b/app/assets/images/emoji/shrug_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png
new file mode 100644
index 00000000000..4e3ca8f8bac
--- /dev/null
+++ b/app/assets/images/emoji/shrug_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png
new file mode 100644
index 00000000000..d1b16a19bb5
--- /dev/null
+++ b/app/assets/images/emoji/shrug_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png
new file mode 100644
index 00000000000..5fbef3f2255
--- /dev/null
+++ b/app/assets/images/emoji/shrug_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png
new file mode 100644
index 00000000000..4af2e28bc5c
--- /dev/null
+++ b/app/assets/images/emoji/shrug_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png
new file mode 100644
index 00000000000..ee2b5a4b519
--- /dev/null
+++ b/app/assets/images/emoji/signal_strength.png
Binary files differ
diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png
new file mode 100644
index 00000000000..371b3acef2c
--- /dev/null
+++ b/app/assets/images/emoji/six.png
Binary files differ
diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png
new file mode 100644
index 00000000000..2eb1707458b
--- /dev/null
+++ b/app/assets/images/emoji/six_pointed_star.png
Binary files differ
diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png
new file mode 100644
index 00000000000..4a2d2c12306
--- /dev/null
+++ b/app/assets/images/emoji/ski.png
Binary files differ
diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png
new file mode 100644
index 00000000000..2eb3bdce2af
--- /dev/null
+++ b/app/assets/images/emoji/skier.png
Binary files differ
diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png
new file mode 100644
index 00000000000..26abb17296a
--- /dev/null
+++ b/app/assets/images/emoji/skull.png
Binary files differ
diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png
new file mode 100644
index 00000000000..b459df9227a
--- /dev/null
+++ b/app/assets/images/emoji/skull_crossbones.png
Binary files differ
diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png
new file mode 100644
index 00000000000..9ecf600d6d8
--- /dev/null
+++ b/app/assets/images/emoji/sleeping.png
Binary files differ
diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png
new file mode 100644
index 00000000000..c739e7fb69b
--- /dev/null
+++ b/app/assets/images/emoji/sleeping_accommodation.png
Binary files differ
diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png
new file mode 100644
index 00000000000..836b4107717
--- /dev/null
+++ b/app/assets/images/emoji/sleepy.png
Binary files differ
diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png
new file mode 100644
index 00000000000..b2f1d983d36
--- /dev/null
+++ b/app/assets/images/emoji/slight_frown.png
Binary files differ
diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png
new file mode 100644
index 00000000000..ddd7d65dd3d
--- /dev/null
+++ b/app/assets/images/emoji/slight_smile.png
Binary files differ
diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png
new file mode 100644
index 00000000000..ee71b6c268c
--- /dev/null
+++ b/app/assets/images/emoji/slot_machine.png
Binary files differ
diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png
new file mode 100644
index 00000000000..b86b5bc4db3
--- /dev/null
+++ b/app/assets/images/emoji/small_blue_diamond.png
Binary files differ
diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png
new file mode 100644
index 00000000000..e1c6ed9b2f8
--- /dev/null
+++ b/app/assets/images/emoji/small_orange_diamond.png
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png
new file mode 100644
index 00000000000..785887c195a
--- /dev/null
+++ b/app/assets/images/emoji/small_red_triangle.png
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png
new file mode 100644
index 00000000000..a83beff1914
--- /dev/null
+++ b/app/assets/images/emoji/small_red_triangle_down.png
Binary files differ
diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png
new file mode 100644
index 00000000000..aa47ffe978c
--- /dev/null
+++ b/app/assets/images/emoji/smile.png
Binary files differ
diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png
new file mode 100644
index 00000000000..6f25f11dd3a
--- /dev/null
+++ b/app/assets/images/emoji/smile_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png
new file mode 100644
index 00000000000..30957a65968
--- /dev/null
+++ b/app/assets/images/emoji/smiley.png
Binary files differ
diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png
new file mode 100644
index 00000000000..163b57a3427
--- /dev/null
+++ b/app/assets/images/emoji/smiley_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png
new file mode 100644
index 00000000000..cc2c5f1ec72
--- /dev/null
+++ b/app/assets/images/emoji/smiling_imp.png
Binary files differ
diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png
new file mode 100644
index 00000000000..87852109988
--- /dev/null
+++ b/app/assets/images/emoji/smirk.png
Binary files differ
diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png
new file mode 100644
index 00000000000..9ac5954c199
--- /dev/null
+++ b/app/assets/images/emoji/smirk_cat.png
Binary files differ
diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png
new file mode 100644
index 00000000000..910f648c8f9
--- /dev/null
+++ b/app/assets/images/emoji/smoking.png
Binary files differ
diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png
new file mode 100644
index 00000000000..f4ea071e2d3
--- /dev/null
+++ b/app/assets/images/emoji/snail.png
Binary files differ
diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png
new file mode 100644
index 00000000000..d0278a28d8c
--- /dev/null
+++ b/app/assets/images/emoji/snake.png
Binary files differ
diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png
new file mode 100644
index 00000000000..ccf07d4b64d
--- /dev/null
+++ b/app/assets/images/emoji/sneezing_face.png
Binary files differ
diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png
new file mode 100644
index 00000000000..6361c0f2c9d
--- /dev/null
+++ b/app/assets/images/emoji/snowboarder.png
Binary files differ
diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png
new file mode 100644
index 00000000000..db319a77ec6
--- /dev/null
+++ b/app/assets/images/emoji/snowflake.png
Binary files differ
diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png
new file mode 100644
index 00000000000..20c177c2aff
--- /dev/null
+++ b/app/assets/images/emoji/snowman.png
Binary files differ
diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png
new file mode 100644
index 00000000000..896f28502af
--- /dev/null
+++ b/app/assets/images/emoji/snowman2.png
Binary files differ
diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png
new file mode 100644
index 00000000000..52e3517a1ee
--- /dev/null
+++ b/app/assets/images/emoji/sob.png
Binary files differ
diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png
new file mode 100644
index 00000000000..28cfa218d6d
--- /dev/null
+++ b/app/assets/images/emoji/soccer.png
Binary files differ
diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png
new file mode 100644
index 00000000000..8cdfd86690d
--- /dev/null
+++ b/app/assets/images/emoji/soon.png
Binary files differ
diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png
new file mode 100644
index 00000000000..d7d8c9953e4
--- /dev/null
+++ b/app/assets/images/emoji/sos.png
Binary files differ
diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png
new file mode 100644
index 00000000000..e75ddca53ba
--- /dev/null
+++ b/app/assets/images/emoji/sound.png
Binary files differ
diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png
new file mode 100644
index 00000000000..2e73f5f32e5
--- /dev/null
+++ b/app/assets/images/emoji/space_invader.png
Binary files differ
diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png
new file mode 100644
index 00000000000..f822f184cb0
--- /dev/null
+++ b/app/assets/images/emoji/spades.png
Binary files differ
diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png
new file mode 100644
index 00000000000..89c24a321f1
--- /dev/null
+++ b/app/assets/images/emoji/spaghetti.png
Binary files differ
diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png
new file mode 100644
index 00000000000..6aa7b6ec9cf
--- /dev/null
+++ b/app/assets/images/emoji/sparkle.png
Binary files differ
diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png
new file mode 100644
index 00000000000..30339cd6e09
--- /dev/null
+++ b/app/assets/images/emoji/sparkler.png
Binary files differ
diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png
new file mode 100644
index 00000000000..169bc10b023
--- /dev/null
+++ b/app/assets/images/emoji/sparkles.png
Binary files differ
diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png
new file mode 100644
index 00000000000..6709269454e
--- /dev/null
+++ b/app/assets/images/emoji/sparkling_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png
new file mode 100644
index 00000000000..9d9e07c974b
--- /dev/null
+++ b/app/assets/images/emoji/speak_no_evil.png
Binary files differ
diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png
new file mode 100644
index 00000000000..7bcffb8fc43
--- /dev/null
+++ b/app/assets/images/emoji/speaker.png
Binary files differ
diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png
new file mode 100644
index 00000000000..2df93aaae09
--- /dev/null
+++ b/app/assets/images/emoji/speaking_head.png
Binary files differ
diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png
new file mode 100644
index 00000000000..a34ef741733
--- /dev/null
+++ b/app/assets/images/emoji/speech_balloon.png
Binary files differ
diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png
new file mode 100644
index 00000000000..74059d12de1
--- /dev/null
+++ b/app/assets/images/emoji/speedboat.png
Binary files differ
diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png
new file mode 100644
index 00000000000..3849fa90b94
--- /dev/null
+++ b/app/assets/images/emoji/spider.png
Binary files differ
diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png
new file mode 100644
index 00000000000..ba448ee7fba
--- /dev/null
+++ b/app/assets/images/emoji/spider_web.png
Binary files differ
diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png
new file mode 100644
index 00000000000..3c4da766aee
--- /dev/null
+++ b/app/assets/images/emoji/spoon.png
Binary files differ
diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png
new file mode 100644
index 00000000000..a729e9584d6
--- /dev/null
+++ b/app/assets/images/emoji/spy.png
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png
new file mode 100644
index 00000000000..2d1c022caee
--- /dev/null
+++ b/app/assets/images/emoji/spy_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png
new file mode 100644
index 00000000000..548b9c26f5d
--- /dev/null
+++ b/app/assets/images/emoji/spy_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png
new file mode 100644
index 00000000000..b023f4b18e1
--- /dev/null
+++ b/app/assets/images/emoji/spy_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png
new file mode 100644
index 00000000000..d8300af492d
--- /dev/null
+++ b/app/assets/images/emoji/spy_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png
new file mode 100644
index 00000000000..ca1462595fa
--- /dev/null
+++ b/app/assets/images/emoji/spy_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png
new file mode 100644
index 00000000000..d2af223f0cb
--- /dev/null
+++ b/app/assets/images/emoji/squid.png
Binary files differ
diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png
new file mode 100644
index 00000000000..00cd6db5e29
--- /dev/null
+++ b/app/assets/images/emoji/stadium.png
Binary files differ
diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png
new file mode 100644
index 00000000000..c930947076e
--- /dev/null
+++ b/app/assets/images/emoji/star.png
Binary files differ
diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png
new file mode 100644
index 00000000000..2f5cba592db
--- /dev/null
+++ b/app/assets/images/emoji/star2.png
Binary files differ
diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png
new file mode 100644
index 00000000000..e182636457d
--- /dev/null
+++ b/app/assets/images/emoji/star_and_crescent.png
Binary files differ
diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png
new file mode 100644
index 00000000000..fc59d0dde24
--- /dev/null
+++ b/app/assets/images/emoji/star_of_david.png
Binary files differ
diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png
new file mode 100644
index 00000000000..aa45384d1c6
--- /dev/null
+++ b/app/assets/images/emoji/stars.png
Binary files differ
diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png
new file mode 100644
index 00000000000..5c26fee529c
--- /dev/null
+++ b/app/assets/images/emoji/station.png
Binary files differ
diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png
new file mode 100644
index 00000000000..05df8289b59
--- /dev/null
+++ b/app/assets/images/emoji/statue_of_liberty.png
Binary files differ
diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png
new file mode 100644
index 00000000000..9ac0d999c4c
--- /dev/null
+++ b/app/assets/images/emoji/steam_locomotive.png
Binary files differ
diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png
new file mode 100644
index 00000000000..6b3f010c17a
--- /dev/null
+++ b/app/assets/images/emoji/stew.png
Binary files differ
diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png
new file mode 100644
index 00000000000..cfa99988ac2
--- /dev/null
+++ b/app/assets/images/emoji/stop_button.png
Binary files differ
diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png
new file mode 100644
index 00000000000..8fae1c9a898
--- /dev/null
+++ b/app/assets/images/emoji/stopwatch.png
Binary files differ
diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png
new file mode 100644
index 00000000000..1017b7433a1
--- /dev/null
+++ b/app/assets/images/emoji/straight_ruler.png
Binary files differ
diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png
new file mode 100644
index 00000000000..7bb86f0b29c
--- /dev/null
+++ b/app/assets/images/emoji/strawberry.png
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png
new file mode 100644
index 00000000000..25757341f96
--- /dev/null
+++ b/app/assets/images/emoji/stuck_out_tongue.png
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
new file mode 100644
index 00000000000..5c0401e9b1d
--- /dev/null
+++ b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
new file mode 100644
index 00000000000..4817eaa3dc6
--- /dev/null
+++ b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
Binary files differ
diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png
new file mode 100644
index 00000000000..a2e10df40a5
--- /dev/null
+++ b/app/assets/images/emoji/stuffed_flatbread.png
Binary files differ
diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png
new file mode 100644
index 00000000000..14a4ea971db
--- /dev/null
+++ b/app/assets/images/emoji/sun_with_face.png
Binary files differ
diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png
new file mode 100644
index 00000000000..08cc07761ea
--- /dev/null
+++ b/app/assets/images/emoji/sunflower.png
Binary files differ
diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png
new file mode 100644
index 00000000000..20011735110
--- /dev/null
+++ b/app/assets/images/emoji/sunglasses.png
Binary files differ
diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png
new file mode 100644
index 00000000000..fd521ae31a7
--- /dev/null
+++ b/app/assets/images/emoji/sunny.png
Binary files differ
diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png
new file mode 100644
index 00000000000..4ad36003c20
--- /dev/null
+++ b/app/assets/images/emoji/sunrise.png
Binary files differ
diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png
new file mode 100644
index 00000000000..2b99307344d
--- /dev/null
+++ b/app/assets/images/emoji/sunrise_over_mountains.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png
new file mode 100644
index 00000000000..3ab017adf4b
--- /dev/null
+++ b/app/assets/images/emoji/surfer.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png
new file mode 100644
index 00000000000..b5faaa524cc
--- /dev/null
+++ b/app/assets/images/emoji/surfer_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png
new file mode 100644
index 00000000000..6d92e412ff1
--- /dev/null
+++ b/app/assets/images/emoji/surfer_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png
new file mode 100644
index 00000000000..f05ef59496e
--- /dev/null
+++ b/app/assets/images/emoji/surfer_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png
new file mode 100644
index 00000000000..35e143d19dc
--- /dev/null
+++ b/app/assets/images/emoji/surfer_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png
new file mode 100644
index 00000000000..38917658eac
--- /dev/null
+++ b/app/assets/images/emoji/surfer_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png
new file mode 100644
index 00000000000..f171fd2f7a1
--- /dev/null
+++ b/app/assets/images/emoji/sushi.png
Binary files differ
diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png
new file mode 100644
index 00000000000..a59d5f48c24
--- /dev/null
+++ b/app/assets/images/emoji/suspension_railway.png
Binary files differ
diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png
new file mode 100644
index 00000000000..f0dae7b7893
--- /dev/null
+++ b/app/assets/images/emoji/sweat.png
Binary files differ
diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png
new file mode 100644
index 00000000000..4106117ebc8
--- /dev/null
+++ b/app/assets/images/emoji/sweat_drops.png
Binary files differ
diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png
new file mode 100644
index 00000000000..cb18d9c899b
--- /dev/null
+++ b/app/assets/images/emoji/sweat_smile.png
Binary files differ
diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png
new file mode 100644
index 00000000000..92a425f2e20
--- /dev/null
+++ b/app/assets/images/emoji/sweet_potato.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png
new file mode 100644
index 00000000000..55b4d72f9a7
--- /dev/null
+++ b/app/assets/images/emoji/swimmer.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png
new file mode 100644
index 00000000000..38441c9ca9a
--- /dev/null
+++ b/app/assets/images/emoji/swimmer_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png
new file mode 100644
index 00000000000..b0d43112444
--- /dev/null
+++ b/app/assets/images/emoji/swimmer_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png
new file mode 100644
index 00000000000..211e77e2aa0
--- /dev/null
+++ b/app/assets/images/emoji/swimmer_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png
new file mode 100644
index 00000000000..f34c34db9d2
--- /dev/null
+++ b/app/assets/images/emoji/swimmer_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png
new file mode 100644
index 00000000000..3e9231ff868
--- /dev/null
+++ b/app/assets/images/emoji/swimmer_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png
new file mode 100644
index 00000000000..ac2fc1f358f
--- /dev/null
+++ b/app/assets/images/emoji/symbols.png
Binary files differ
diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png
new file mode 100644
index 00000000000..ee347904c80
--- /dev/null
+++ b/app/assets/images/emoji/synagogue.png
Binary files differ
diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png
new file mode 100644
index 00000000000..71c1a9528d5
--- /dev/null
+++ b/app/assets/images/emoji/syringe.png
Binary files differ
diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png
new file mode 100644
index 00000000000..10e847a4619
--- /dev/null
+++ b/app/assets/images/emoji/taco.png
Binary files differ
diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png
new file mode 100644
index 00000000000..0244d60f269
--- /dev/null
+++ b/app/assets/images/emoji/tada.png
Binary files differ
diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png
new file mode 100644
index 00000000000..46fcb3a1aac
--- /dev/null
+++ b/app/assets/images/emoji/tanabata_tree.png
Binary files differ
diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png
new file mode 100644
index 00000000000..ab14e5378db
--- /dev/null
+++ b/app/assets/images/emoji/tangerine.png
Binary files differ
diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png
new file mode 100644
index 00000000000..b2a370df42b
--- /dev/null
+++ b/app/assets/images/emoji/taurus.png
Binary files differ
diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png
new file mode 100644
index 00000000000..55f4cc84797
--- /dev/null
+++ b/app/assets/images/emoji/taxi.png
Binary files differ
diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png
new file mode 100644
index 00000000000..b53b98f0c45
--- /dev/null
+++ b/app/assets/images/emoji/tea.png
Binary files differ
diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png
new file mode 100644
index 00000000000..a1e69f566bc
--- /dev/null
+++ b/app/assets/images/emoji/telephone.png
Binary files differ
diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png
new file mode 100644
index 00000000000..69388316c35
--- /dev/null
+++ b/app/assets/images/emoji/telephone_receiver.png
Binary files differ
diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png
new file mode 100644
index 00000000000..d63154614b5
--- /dev/null
+++ b/app/assets/images/emoji/telescope.png
Binary files differ
diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png
new file mode 100644
index 00000000000..782d4004962
--- /dev/null
+++ b/app/assets/images/emoji/ten.png
Binary files differ
diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png
new file mode 100644
index 00000000000..7e68ba8f301
--- /dev/null
+++ b/app/assets/images/emoji/tennis.png
Binary files differ
diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png
new file mode 100644
index 00000000000..3fddcfc56eb
--- /dev/null
+++ b/app/assets/images/emoji/tent.png
Binary files differ
diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png
new file mode 100644
index 00000000000..b1147392426
--- /dev/null
+++ b/app/assets/images/emoji/thermometer.png
Binary files differ
diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png
new file mode 100644
index 00000000000..8fc57387563
--- /dev/null
+++ b/app/assets/images/emoji/thermometer_face.png
Binary files differ
diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png
new file mode 100644
index 00000000000..c18f6fd14ad
--- /dev/null
+++ b/app/assets/images/emoji/thinking.png
Binary files differ
diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png
new file mode 100644
index 00000000000..636e04a5950
--- /dev/null
+++ b/app/assets/images/emoji/third_place.png
Binary files differ
diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png
new file mode 100644
index 00000000000..72fe8fa7022
--- /dev/null
+++ b/app/assets/images/emoji/thought_balloon.png
Binary files differ
diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png
new file mode 100644
index 00000000000..dbaa6183e72
--- /dev/null
+++ b/app/assets/images/emoji/three.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png
new file mode 100644
index 00000000000..b63da2f20a8
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png
new file mode 100644
index 00000000000..a1631af8e92
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png
new file mode 100644
index 00000000000..85fff82d595
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png
new file mode 100644
index 00000000000..eeba3be80fd
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png
new file mode 100644
index 00000000000..1addafdaed0
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png
new file mode 100644
index 00000000000..37ec07b5721
--- /dev/null
+++ b/app/assets/images/emoji/thumbsdown_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png
new file mode 100644
index 00000000000..f9e6f13a34f
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png
new file mode 100644
index 00000000000..39684cd5cc7
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png
new file mode 100644
index 00000000000..a9b59723573
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png
new file mode 100644
index 00000000000..c5e29167015
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png
new file mode 100644
index 00000000000..5bf4857a884
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png
new file mode 100644
index 00000000000..d829f787c61
--- /dev/null
+++ b/app/assets/images/emoji/thumbsup_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png
new file mode 100644
index 00000000000..31a26a1b6ee
--- /dev/null
+++ b/app/assets/images/emoji/thunder_cloud_rain.png
Binary files differ
diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png
new file mode 100644
index 00000000000..605936bb6b3
--- /dev/null
+++ b/app/assets/images/emoji/ticket.png
Binary files differ
diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png
new file mode 100644
index 00000000000..e510f4a7a50
--- /dev/null
+++ b/app/assets/images/emoji/tickets.png
Binary files differ
diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png
new file mode 100644
index 00000000000..a4d3ef086d4
--- /dev/null
+++ b/app/assets/images/emoji/tiger.png
Binary files differ
diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png
new file mode 100644
index 00000000000..871a8b74d56
--- /dev/null
+++ b/app/assets/images/emoji/tiger2.png
Binary files differ
diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png
new file mode 100644
index 00000000000..8a3be574c24
--- /dev/null
+++ b/app/assets/images/emoji/timer.png
Binary files differ
diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png
new file mode 100644
index 00000000000..4e01eff5b23
--- /dev/null
+++ b/app/assets/images/emoji/tired_face.png
Binary files differ
diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png
new file mode 100644
index 00000000000..7a0c44a2c2b
--- /dev/null
+++ b/app/assets/images/emoji/tm.png
Binary files differ
diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png
new file mode 100644
index 00000000000..1392f761835
--- /dev/null
+++ b/app/assets/images/emoji/toilet.png
Binary files differ
diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png
new file mode 100644
index 00000000000..37df7fc65b1
--- /dev/null
+++ b/app/assets/images/emoji/tokyo_tower.png
Binary files differ
diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png
new file mode 100644
index 00000000000..497da8f6b22
--- /dev/null
+++ b/app/assets/images/emoji/tomato.png
Binary files differ
diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png
new file mode 100644
index 00000000000..c395f3d0d68
--- /dev/null
+++ b/app/assets/images/emoji/tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png
new file mode 100644
index 00000000000..080847431c1
--- /dev/null
+++ b/app/assets/images/emoji/tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png
new file mode 100644
index 00000000000..482dd403475
--- /dev/null
+++ b/app/assets/images/emoji/tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png
new file mode 100644
index 00000000000..5cae8bb20b0
--- /dev/null
+++ b/app/assets/images/emoji/tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png
new file mode 100644
index 00000000000..49d1a8c3a64
--- /dev/null
+++ b/app/assets/images/emoji/tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png
new file mode 100644
index 00000000000..70ce9c1225f
--- /dev/null
+++ b/app/assets/images/emoji/tongue.png
Binary files differ
diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png
new file mode 100644
index 00000000000..3c6049273a9
--- /dev/null
+++ b/app/assets/images/emoji/tools.png
Binary files differ
diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png
new file mode 100644
index 00000000000..49dea8c08b5
--- /dev/null
+++ b/app/assets/images/emoji/top.png
Binary files differ
diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png
new file mode 100644
index 00000000000..131b657b109
--- /dev/null
+++ b/app/assets/images/emoji/tophat.png
Binary files differ
diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png
new file mode 100644
index 00000000000..f8880d33bab
--- /dev/null
+++ b/app/assets/images/emoji/track_next.png
Binary files differ
diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png
new file mode 100644
index 00000000000..1ffd0566cfc
--- /dev/null
+++ b/app/assets/images/emoji/track_previous.png
Binary files differ
diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png
new file mode 100644
index 00000000000..3bea84ad7ce
--- /dev/null
+++ b/app/assets/images/emoji/trackball.png
Binary files differ
diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png
new file mode 100644
index 00000000000..c1bf8cae44f
--- /dev/null
+++ b/app/assets/images/emoji/tractor.png
Binary files differ
diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png
new file mode 100644
index 00000000000..6b312285b00
--- /dev/null
+++ b/app/assets/images/emoji/traffic_light.png
Binary files differ
diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png
new file mode 100644
index 00000000000..3c80321f7e8
--- /dev/null
+++ b/app/assets/images/emoji/train.png
Binary files differ
diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png
new file mode 100644
index 00000000000..367c7bc5d39
--- /dev/null
+++ b/app/assets/images/emoji/train2.png
Binary files differ
diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png
new file mode 100644
index 00000000000..b6f0e69038f
--- /dev/null
+++ b/app/assets/images/emoji/tram.png
Binary files differ
diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png
new file mode 100644
index 00000000000..c12d8b06886
--- /dev/null
+++ b/app/assets/images/emoji/triangular_flag_on_post.png
Binary files differ
diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png
new file mode 100644
index 00000000000..77dee9ee843
--- /dev/null
+++ b/app/assets/images/emoji/triangular_ruler.png
Binary files differ
diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png
new file mode 100644
index 00000000000..777a1dad121
--- /dev/null
+++ b/app/assets/images/emoji/trident.png
Binary files differ
diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png
new file mode 100644
index 00000000000..0be7a501969
--- /dev/null
+++ b/app/assets/images/emoji/triumph.png
Binary files differ
diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png
new file mode 100644
index 00000000000..139a9931b52
--- /dev/null
+++ b/app/assets/images/emoji/trolleybus.png
Binary files differ
diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png
new file mode 100644
index 00000000000..ac2895c1896
--- /dev/null
+++ b/app/assets/images/emoji/trophy.png
Binary files differ
diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png
new file mode 100644
index 00000000000..cd714f81b36
--- /dev/null
+++ b/app/assets/images/emoji/tropical_drink.png
Binary files differ
diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png
new file mode 100644
index 00000000000..252105235a6
--- /dev/null
+++ b/app/assets/images/emoji/tropical_fish.png
Binary files differ
diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png
new file mode 100644
index 00000000000..130de047f8b
--- /dev/null
+++ b/app/assets/images/emoji/truck.png
Binary files differ
diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png
new file mode 100644
index 00000000000..864ccbcd04a
--- /dev/null
+++ b/app/assets/images/emoji/trumpet.png
Binary files differ
diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png
new file mode 100644
index 00000000000..f799d75c182
--- /dev/null
+++ b/app/assets/images/emoji/tulip.png
Binary files differ
diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png
new file mode 100644
index 00000000000..7bf09229879
--- /dev/null
+++ b/app/assets/images/emoji/tumbler_glass.png
Binary files differ
diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png
new file mode 100644
index 00000000000..344af94c9ec
--- /dev/null
+++ b/app/assets/images/emoji/turkey.png
Binary files differ
diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png
new file mode 100644
index 00000000000..c22f7519fe8
--- /dev/null
+++ b/app/assets/images/emoji/turtle.png
Binary files differ
diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png
new file mode 100644
index 00000000000..999f1fb5c6d
--- /dev/null
+++ b/app/assets/images/emoji/tv.png
Binary files differ
diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png
new file mode 100644
index 00000000000..5904badde65
--- /dev/null
+++ b/app/assets/images/emoji/twisted_rightwards_arrows.png
Binary files differ
diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png
new file mode 100644
index 00000000000..927339c9bff
--- /dev/null
+++ b/app/assets/images/emoji/two.png
Binary files differ
diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png
new file mode 100644
index 00000000000..4d8c3386042
--- /dev/null
+++ b/app/assets/images/emoji/two_hearts.png
Binary files differ
diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png
new file mode 100644
index 00000000000..a511fda822a
--- /dev/null
+++ b/app/assets/images/emoji/two_men_holding_hands.png
Binary files differ
diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png
new file mode 100644
index 00000000000..b077cd3e40f
--- /dev/null
+++ b/app/assets/images/emoji/two_women_holding_hands.png
Binary files differ
diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png
new file mode 100644
index 00000000000..c4f837fe684
--- /dev/null
+++ b/app/assets/images/emoji/u5272.png
Binary files differ
diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png
new file mode 100644
index 00000000000..8375ad9d9af
--- /dev/null
+++ b/app/assets/images/emoji/u5408.png
Binary files differ
diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png
new file mode 100644
index 00000000000..d21cb30eaf3
--- /dev/null
+++ b/app/assets/images/emoji/u55b6.png
Binary files differ
diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png
new file mode 100644
index 00000000000..078e23e4ff3
--- /dev/null
+++ b/app/assets/images/emoji/u6307.png
Binary files differ
diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png
new file mode 100644
index 00000000000..c41bd36a26a
--- /dev/null
+++ b/app/assets/images/emoji/u6708.png
Binary files differ
diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png
new file mode 100644
index 00000000000..a4510de41c0
--- /dev/null
+++ b/app/assets/images/emoji/u6709.png
Binary files differ
diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png
new file mode 100644
index 00000000000..f9dea8b8833
--- /dev/null
+++ b/app/assets/images/emoji/u6e80.png
Binary files differ
diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png
new file mode 100644
index 00000000000..d3a19b420de
--- /dev/null
+++ b/app/assets/images/emoji/u7121.png
Binary files differ
diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png
new file mode 100644
index 00000000000..6b7af0ee222
--- /dev/null
+++ b/app/assets/images/emoji/u7533.png
Binary files differ
diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png
new file mode 100644
index 00000000000..4c704e03433
--- /dev/null
+++ b/app/assets/images/emoji/u7981.png
Binary files differ
diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png
new file mode 100644
index 00000000000..47966c1ea93
--- /dev/null
+++ b/app/assets/images/emoji/u7a7a.png
Binary files differ
diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png
new file mode 100644
index 00000000000..5b35b7ff6a4
--- /dev/null
+++ b/app/assets/images/emoji/umbrella.png
Binary files differ
diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png
new file mode 100644
index 00000000000..97fe859e74f
--- /dev/null
+++ b/app/assets/images/emoji/umbrella2.png
Binary files differ
diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png
new file mode 100644
index 00000000000..25e3677f2eb
--- /dev/null
+++ b/app/assets/images/emoji/unamused.png
Binary files differ
diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png
new file mode 100644
index 00000000000..6dfe6da51e2
--- /dev/null
+++ b/app/assets/images/emoji/underage.png
Binary files differ
diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png
new file mode 100644
index 00000000000..05a97969f7e
--- /dev/null
+++ b/app/assets/images/emoji/unicorn.png
Binary files differ
diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png
new file mode 100644
index 00000000000..4a74a693911
--- /dev/null
+++ b/app/assets/images/emoji/unlock.png
Binary files differ
diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png
new file mode 100644
index 00000000000..0d42142ba04
--- /dev/null
+++ b/app/assets/images/emoji/up.png
Binary files differ
diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png
new file mode 100644
index 00000000000..128f31c9828
--- /dev/null
+++ b/app/assets/images/emoji/upside_down.png
Binary files differ
diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png
new file mode 100644
index 00000000000..6b5b3503438
--- /dev/null
+++ b/app/assets/images/emoji/urn.png
Binary files differ
diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png
new file mode 100644
index 00000000000..70c5516ffee
--- /dev/null
+++ b/app/assets/images/emoji/v.png
Binary files differ
diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png
new file mode 100644
index 00000000000..6ac54a745f4
--- /dev/null
+++ b/app/assets/images/emoji/v_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png
new file mode 100644
index 00000000000..6dd9669866d
--- /dev/null
+++ b/app/assets/images/emoji/v_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png
new file mode 100644
index 00000000000..a615e53f02f
--- /dev/null
+++ b/app/assets/images/emoji/v_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png
new file mode 100644
index 00000000000..33a34bd5a78
--- /dev/null
+++ b/app/assets/images/emoji/v_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png
new file mode 100644
index 00000000000..45ad14b6c9c
--- /dev/null
+++ b/app/assets/images/emoji/v_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png
new file mode 100644
index 00000000000..8085973eecf
--- /dev/null
+++ b/app/assets/images/emoji/vertical_traffic_light.png
Binary files differ
diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png
new file mode 100644
index 00000000000..b9eb78ecd92
--- /dev/null
+++ b/app/assets/images/emoji/vhs.png
Binary files differ
diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png
new file mode 100644
index 00000000000..cc46510e48e
--- /dev/null
+++ b/app/assets/images/emoji/vibration_mode.png
Binary files differ
diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png
new file mode 100644
index 00000000000..85b300d425c
--- /dev/null
+++ b/app/assets/images/emoji/video_camera.png
Binary files differ
diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png
new file mode 100644
index 00000000000..316a9106a55
--- /dev/null
+++ b/app/assets/images/emoji/video_game.png
Binary files differ
diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png
new file mode 100644
index 00000000000..e1e76cce242
--- /dev/null
+++ b/app/assets/images/emoji/violin.png
Binary files differ
diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png
new file mode 100644
index 00000000000..a6b56c2cb5e
--- /dev/null
+++ b/app/assets/images/emoji/virgo.png
Binary files differ
diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png
new file mode 100644
index 00000000000..931d569294c
--- /dev/null
+++ b/app/assets/images/emoji/volcano.png
Binary files differ
diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png
new file mode 100644
index 00000000000..7a0e49d4b07
--- /dev/null
+++ b/app/assets/images/emoji/volleyball.png
Binary files differ
diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png
new file mode 100644
index 00000000000..e1180f4a464
--- /dev/null
+++ b/app/assets/images/emoji/vs.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png
new file mode 100644
index 00000000000..54728bcaf5c
--- /dev/null
+++ b/app/assets/images/emoji/vulcan.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png
new file mode 100644
index 00000000000..8aff5d8fa16
--- /dev/null
+++ b/app/assets/images/emoji/vulcan_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png
new file mode 100644
index 00000000000..82b7ad519b4
--- /dev/null
+++ b/app/assets/images/emoji/vulcan_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png
new file mode 100644
index 00000000000..d1400e1dd28
--- /dev/null
+++ b/app/assets/images/emoji/vulcan_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png
new file mode 100644
index 00000000000..47e2b280148
--- /dev/null
+++ b/app/assets/images/emoji/vulcan_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png
new file mode 100644
index 00000000000..60b5c6077be
--- /dev/null
+++ b/app/assets/images/emoji/vulcan_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png
new file mode 100644
index 00000000000..06dc169a3fd
--- /dev/null
+++ b/app/assets/images/emoji/walking.png
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png
new file mode 100644
index 00000000000..4e391b45a0b
--- /dev/null
+++ b/app/assets/images/emoji/walking_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png
new file mode 100644
index 00000000000..31f94a1bce1
--- /dev/null
+++ b/app/assets/images/emoji/walking_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png
new file mode 100644
index 00000000000..f7ed8e39c2e
--- /dev/null
+++ b/app/assets/images/emoji/walking_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png
new file mode 100644
index 00000000000..e58dc04c7b2
--- /dev/null
+++ b/app/assets/images/emoji/walking_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png
new file mode 100644
index 00000000000..ba4e1b58fcb
--- /dev/null
+++ b/app/assets/images/emoji/walking_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png
new file mode 100644
index 00000000000..cf68706b871
--- /dev/null
+++ b/app/assets/images/emoji/waning_crescent_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png
new file mode 100644
index 00000000000..24e16266119
--- /dev/null
+++ b/app/assets/images/emoji/waning_gibbous_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png
new file mode 100644
index 00000000000..35691c2ed97
--- /dev/null
+++ b/app/assets/images/emoji/warning.png
Binary files differ
diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png
new file mode 100644
index 00000000000..2b3c484b498
--- /dev/null
+++ b/app/assets/images/emoji/wastebasket.png
Binary files differ
diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png
new file mode 100644
index 00000000000..64819bc6e21
--- /dev/null
+++ b/app/assets/images/emoji/watch.png
Binary files differ
diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png
new file mode 100644
index 00000000000..80446615caf
--- /dev/null
+++ b/app/assets/images/emoji/water_buffalo.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png
new file mode 100644
index 00000000000..cb44576780d
--- /dev/null
+++ b/app/assets/images/emoji/water_polo.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png
new file mode 100644
index 00000000000..bed1a908d6a
--- /dev/null
+++ b/app/assets/images/emoji/water_polo_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png
new file mode 100644
index 00000000000..ec5a43b4d4a
--- /dev/null
+++ b/app/assets/images/emoji/water_polo_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png
new file mode 100644
index 00000000000..b081a4a5a96
--- /dev/null
+++ b/app/assets/images/emoji/water_polo_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png
new file mode 100644
index 00000000000..82cfbc3b0c7
--- /dev/null
+++ b/app/assets/images/emoji/water_polo_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png
new file mode 100644
index 00000000000..bd3366eb06c
--- /dev/null
+++ b/app/assets/images/emoji/water_polo_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png
new file mode 100644
index 00000000000..0761488b4c9
--- /dev/null
+++ b/app/assets/images/emoji/watermelon.png
Binary files differ
diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png
new file mode 100644
index 00000000000..e0cd79b45f5
--- /dev/null
+++ b/app/assets/images/emoji/wave.png
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png
new file mode 100644
index 00000000000..6b2b34b106e
--- /dev/null
+++ b/app/assets/images/emoji/wave_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png
new file mode 100644
index 00000000000..b857119732e
--- /dev/null
+++ b/app/assets/images/emoji/wave_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png
new file mode 100644
index 00000000000..6283b670f43
--- /dev/null
+++ b/app/assets/images/emoji/wave_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png
new file mode 100644
index 00000000000..fe6b2baa747
--- /dev/null
+++ b/app/assets/images/emoji/wave_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png
new file mode 100644
index 00000000000..4bd168ebb78
--- /dev/null
+++ b/app/assets/images/emoji/wave_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png
new file mode 100644
index 00000000000..001c8d6e47d
--- /dev/null
+++ b/app/assets/images/emoji/wavy_dash.png
Binary files differ
diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png
new file mode 100644
index 00000000000..687125173d9
--- /dev/null
+++ b/app/assets/images/emoji/waxing_crescent_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png
new file mode 100644
index 00000000000..3a808156318
--- /dev/null
+++ b/app/assets/images/emoji/waxing_gibbous_moon.png
Binary files differ
diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png
new file mode 100644
index 00000000000..aa433e84ba6
--- /dev/null
+++ b/app/assets/images/emoji/wc.png
Binary files differ
diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png
new file mode 100644
index 00000000000..98bfbd24a16
--- /dev/null
+++ b/app/assets/images/emoji/weary.png
Binary files differ
diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png
new file mode 100644
index 00000000000..d0d8aa0bfae
--- /dev/null
+++ b/app/assets/images/emoji/wedding.png
Binary files differ
diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png
new file mode 100644
index 00000000000..9f19b44257c
--- /dev/null
+++ b/app/assets/images/emoji/whale.png
Binary files differ
diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png
new file mode 100644
index 00000000000..0df9d3c73a4
--- /dev/null
+++ b/app/assets/images/emoji/whale2.png
Binary files differ
diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png
new file mode 100644
index 00000000000..3666db0016b
--- /dev/null
+++ b/app/assets/images/emoji/wheel_of_dharma.png
Binary files differ
diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png
new file mode 100644
index 00000000000..4e5b2698eac
--- /dev/null
+++ b/app/assets/images/emoji/wheelchair.png
Binary files differ
diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png
new file mode 100644
index 00000000000..e55f087e544
--- /dev/null
+++ b/app/assets/images/emoji/white_check_mark.png
Binary files differ
diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png
new file mode 100644
index 00000000000..c19e15684dd
--- /dev/null
+++ b/app/assets/images/emoji/white_circle.png
Binary files differ
diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png
new file mode 100644
index 00000000000..d6af8b60077
--- /dev/null
+++ b/app/assets/images/emoji/white_flower.png
Binary files differ
diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png
new file mode 100644
index 00000000000..6f06c1c79de
--- /dev/null
+++ b/app/assets/images/emoji/white_large_square.png
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png
new file mode 100644
index 00000000000..ae874126750
--- /dev/null
+++ b/app/assets/images/emoji/white_medium_small_square.png
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png
new file mode 100644
index 00000000000..8daacf57059
--- /dev/null
+++ b/app/assets/images/emoji/white_medium_square.png
Binary files differ
diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png
new file mode 100644
index 00000000000..d7ebdb0c0ed
--- /dev/null
+++ b/app/assets/images/emoji/white_small_square.png
Binary files differ
diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png
new file mode 100644
index 00000000000..934b1cedfd2
--- /dev/null
+++ b/app/assets/images/emoji/white_square_button.png
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png
new file mode 100644
index 00000000000..0a4cc100269
--- /dev/null
+++ b/app/assets/images/emoji/white_sun_cloud.png
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png
new file mode 100644
index 00000000000..491f9ca4839
--- /dev/null
+++ b/app/assets/images/emoji/white_sun_rain_cloud.png
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png
new file mode 100644
index 00000000000..cead0bfa521
--- /dev/null
+++ b/app/assets/images/emoji/white_sun_small_cloud.png
Binary files differ
diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png
new file mode 100644
index 00000000000..62412b143ae
--- /dev/null
+++ b/app/assets/images/emoji/wilted_rose.png
Binary files differ
diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png
new file mode 100644
index 00000000000..df81b652eb6
--- /dev/null
+++ b/app/assets/images/emoji/wind_blowing_face.png
Binary files differ
diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png
new file mode 100644
index 00000000000..3c9ef3a95f6
--- /dev/null
+++ b/app/assets/images/emoji/wind_chime.png
Binary files differ
diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png
new file mode 100644
index 00000000000..3cc98689192
--- /dev/null
+++ b/app/assets/images/emoji/wine_glass.png
Binary files differ
diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png
new file mode 100644
index 00000000000..7ea7810a37d
--- /dev/null
+++ b/app/assets/images/emoji/wink.png
Binary files differ
diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png
new file mode 100644
index 00000000000..ba7220f2de9
--- /dev/null
+++ b/app/assets/images/emoji/wolf.png
Binary files differ
diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png
new file mode 100644
index 00000000000..ece440e7a61
--- /dev/null
+++ b/app/assets/images/emoji/woman.png
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png
new file mode 100644
index 00000000000..ff089b8889b
--- /dev/null
+++ b/app/assets/images/emoji/woman_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png
new file mode 100644
index 00000000000..0719c378016
--- /dev/null
+++ b/app/assets/images/emoji/woman_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png
new file mode 100644
index 00000000000..5672e2fd52d
--- /dev/null
+++ b/app/assets/images/emoji/woman_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png
new file mode 100644
index 00000000000..5754aab558b
--- /dev/null
+++ b/app/assets/images/emoji/woman_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png
new file mode 100644
index 00000000000..fc252af3a39
--- /dev/null
+++ b/app/assets/images/emoji/woman_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png
new file mode 100644
index 00000000000..01410dc8107
--- /dev/null
+++ b/app/assets/images/emoji/womans_clothes.png
Binary files differ
diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png
new file mode 100644
index 00000000000..b837b6a2e47
--- /dev/null
+++ b/app/assets/images/emoji/womans_hat.png
Binary files differ
diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png
new file mode 100644
index 00000000000..d4ecc22e7b3
--- /dev/null
+++ b/app/assets/images/emoji/womens.png
Binary files differ
diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png
new file mode 100644
index 00000000000..7074afcf5b7
--- /dev/null
+++ b/app/assets/images/emoji/worried.png
Binary files differ
diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png
new file mode 100644
index 00000000000..c16b7439697
--- /dev/null
+++ b/app/assets/images/emoji/wrench.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png
new file mode 100644
index 00000000000..71e67cfad85
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png
new file mode 100644
index 00000000000..379070fd03b
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png
new file mode 100644
index 00000000000..6863ea9209d
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png
new file mode 100644
index 00000000000..b7e62910127
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png
new file mode 100644
index 00000000000..750f9589233
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png
new file mode 100644
index 00000000000..36ab9bb3f42
--- /dev/null
+++ b/app/assets/images/emoji/wrestlers_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png
new file mode 100644
index 00000000000..85639f8ac40
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png
new file mode 100644
index 00000000000..7923d8ebb17
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand_tone1.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png
new file mode 100644
index 00000000000..bcb304e15d2
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand_tone2.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png
new file mode 100644
index 00000000000..fd885fd2d90
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand_tone3.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png
new file mode 100644
index 00000000000..d065b8c64ab
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand_tone4.png
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png
new file mode 100644
index 00000000000..a44b3dd757c
--- /dev/null
+++ b/app/assets/images/emoji/writing_hand_tone5.png
Binary files differ
diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png
new file mode 100644
index 00000000000..9f9ed0f7ad2
--- /dev/null
+++ b/app/assets/images/emoji/x.png
Binary files differ
diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png
new file mode 100644
index 00000000000..7901a9d0103
--- /dev/null
+++ b/app/assets/images/emoji/yellow_heart.png
Binary files differ
diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png
new file mode 100644
index 00000000000..63ee4799d66
--- /dev/null
+++ b/app/assets/images/emoji/yen.png
Binary files differ
diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png
new file mode 100644
index 00000000000..f2900f6338f
--- /dev/null
+++ b/app/assets/images/emoji/yin_yang.png
Binary files differ
diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png
new file mode 100644
index 00000000000..2df15753ca1
--- /dev/null
+++ b/app/assets/images/emoji/yum.png
Binary files differ
diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png
new file mode 100644
index 00000000000..47e68e48e49
--- /dev/null
+++ b/app/assets/images/emoji/zap.png
Binary files differ
diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png
new file mode 100644
index 00000000000..13aca83e018
--- /dev/null
+++ b/app/assets/images/emoji/zero.png
Binary files differ
diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png
new file mode 100644
index 00000000000..f8ced2502a7
--- /dev/null
+++ b/app/assets/images/emoji/zipper_mouth.png
Binary files differ
diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png
new file mode 100644
index 00000000000..9bc72b4469f
--- /dev/null
+++ b/app/assets/images/emoji/zzz.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index dc9cae1d44c..b0fa9e1139e 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg
new file mode 100644
index 00000000000..c4d8e65122d
--- /dev/null
+++ b/app/assets/images/icon-merge-request-unmerged.svg
@@ -0,0 +1 @@
+<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg>
diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif
new file mode 100644
index 00000000000..3f4ef31947b
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_footer_logo.gif
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif
new file mode 100644
index 00000000000..387628f831c
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_header_logo.gif
Binary files differ
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
new file mode 100644
index 00000000000..346de4ad11e
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js
@@ -0,0 +1,37 @@
+const MAX_MESSAGE_LENGTH = 500;
+const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
+
+class AbuseReports {
+ constructor() {
+ $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+ $(document)
+ .off('click', MESSAGE_CELL_SELECTOR)
+ .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+ }
+
+ truncateLongMessage() {
+ const $messageCellElement = $(this);
+ const reportMessage = $messageCellElement.text();
+ if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+ $messageCellElement.data('original-message', reportMessage);
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ }
+ }
+
+ toggleMessageTruncation() {
+ const $messageCellElement = $(this);
+ const originalMessage = $messageCellElement.data('original-message');
+ if (!originalMessage) return;
+ if ($messageCellElement.data('message-truncated') === 'true') {
+ $messageCellElement.data('message-truncated', 'false');
+ $messageCellElement.text(originalMessage);
+ } else {
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ }
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.AbuseReports = AbuseReports;
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
deleted file mode 100644
index 8a260aae1b1..00000000000
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ /dev/null
@@ -1,40 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-((global) => {
- const MAX_MESSAGE_LENGTH = 500;
- const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-
- class AbuseReports {
- constructor() {
- $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
- $(document)
- .off('click', MESSAGE_CELL_SELECTOR)
- .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
- }
-
- truncateLongMessage() {
- const $messageCellElement = $(this);
- const reportMessage = $messageCellElement.text();
- if (reportMessage.length > MAX_MESSAGE_LENGTH) {
- $messageCellElement.data('original-message', reportMessage);
- $messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
- }
- }
-
- toggleMessageTruncation() {
- const $messageCellElement = $(this);
- const originalMessage = $messageCellElement.data('original-message');
- if (!originalMessage) return;
- if ($messageCellElement.data('message-truncated') === 'true') {
- $messageCellElement.data('message-truncated', 'false');
- $messageCellElement.text(originalMessage);
- } else {
- $messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
- }
- }
- }
-
- global.AbuseReports = AbuseReports;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
new file mode 100644
index 00000000000..aebda7780e1
--- /dev/null
+++ b/app/assets/javascripts/activities.js
@@ -0,0 +1,36 @@
+/* eslint-disable no-param-reassign, class-methods-use-this */
+/* global Pager */
+/* global Cookies */
+
+class Activities {
+ constructor() {
+ Pager.init(20, true, false, this.updateTooltips);
+ $('.event-filter-link').on('click', (e) => {
+ e.preventDefault();
+ this.toggleFilter(e.currentTarget);
+ this.reloadActivities();
+ });
+ }
+
+ updateTooltips() {
+ gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ }
+
+ reloadActivities() {
+ $('.content_list').html('');
+ Pager.init(20, true, false, this.updateTooltips);
+ }
+
+ toggleFilter(sender) {
+ const $sender = $(sender);
+ const filter = $sender.attr('id').split('_')[0];
+
+ $('.event-filter .active').removeClass('active');
+ Cookies.set('event_filter', filter);
+
+ $sender.closest('li').toggleClass('active');
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.Activities = Activities;
diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6
deleted file mode 100644
index 648cb4d5d85..00000000000
--- a/app/assets/javascripts/activities.js.es6
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable no-param-reassign, class-methods-use-this */
-/* global Pager */
-/* global Cookies */
-
-((global) => {
- class Activities {
- constructor() {
- Pager.init(20, true, false, this.updateTooltips);
- $('.event-filter-link').on('click', (e) => {
- e.preventDefault();
- this.toggleFilter(e.currentTarget);
- this.reloadActivities();
- });
- }
-
- updateTooltips() {
- gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
- }
-
- reloadActivities() {
- $('.content_list').html('');
- Pager.init(20, true, false, this.updateTooltips);
- }
-
- toggleFilter(sender) {
- const $sender = $(sender);
- const filter = $sender.attr('id').split('_')[0];
-
- $('.event-filter .active').removeClass('active');
- Cookies.set('event_filter', filter);
-
- $sender.closest('li').toggleClass('active');
- }
- }
-
- global.Activities = Activities;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index aaed74d6073..34669dd13d6 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,64 +1,62 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
-(function() {
- this.Admin = (function() {
- function Admin() {
- var modal, showBlacklistType;
- $('input#user_force_random_password').on('change', function(elem) {
- var elems;
- elems = $('#user_password, #user_password_confirmation');
- if ($(this).attr('checked')) {
- return elems.val('').attr('disabled', true);
- } else {
- return elems.removeAttr('disabled');
- }
- });
- $('body').on('click', '.js-toggle-colors-link', function(e) {
- e.preventDefault();
- return $('.js-toggle-colors-container').toggle();
- });
- $('.log-tabs a').click(function(e) {
- e.preventDefault();
- return $(this).tab('show');
- });
- $('.log-bottom').click(function(e) {
- var visible_log;
- e.preventDefault();
- visible_log = $(".file-content:visible");
- return visible_log.animate({
- scrollTop: visible_log.find('ol').height()
- }, "fast");
- });
- modal = $('.change-owner-holder');
- $('.change-owner-link').bind("click", function(e) {
- e.preventDefault();
- $(this).hide();
- return modal.show();
- });
- $('.change-owner-cancel-link').bind("click", function(e) {
- e.preventDefault();
- modal.hide();
- return $('.change-owner-link').show();
- });
- $('li.project_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- $('li.group_member').bind('ajax:success', function() {
- return gl.utils.refreshCurrentPage();
- });
- showBlacklistType = function() {
- if ($("input[name='blacklist_type']:checked").val() === 'file') {
- $('.blacklist-file').show();
- return $('.blacklist-raw').hide();
- } else {
- $('.blacklist-file').hide();
- return $('.blacklist-raw').show();
- }
- };
- $("input[name='blacklist_type']").click(showBlacklistType);
- showBlacklistType();
- }
+window.Admin = (function() {
+ function Admin() {
+ var modal, showBlacklistType;
+ $('input#user_force_random_password').on('change', function(elem) {
+ var elems;
+ elems = $('#user_password, #user_password_confirmation');
+ if ($(this).attr('checked')) {
+ return elems.val('').attr('disabled', true);
+ } else {
+ return elems.removeAttr('disabled');
+ }
+ });
+ $('body').on('click', '.js-toggle-colors-link', function(e) {
+ e.preventDefault();
+ return $('.js-toggle-colors-container').toggle();
+ });
+ $('.log-tabs a').click(function(e) {
+ e.preventDefault();
+ return $(this).tab('show');
+ });
+ $('.log-bottom').click(function(e) {
+ var visible_log;
+ e.preventDefault();
+ visible_log = $(".file-content:visible");
+ return visible_log.animate({
+ scrollTop: visible_log.find('ol').height()
+ }, "fast");
+ });
+ modal = $('.change-owner-holder');
+ $('.change-owner-link').bind("click", function(e) {
+ e.preventDefault();
+ $(this).hide();
+ return modal.show();
+ });
+ $('.change-owner-cancel-link').bind("click", function(e) {
+ e.preventDefault();
+ modal.hide();
+ return $('.change-owner-link').show();
+ });
+ $('li.project_member').bind('ajax:success', function() {
+ return gl.utils.refreshCurrentPage();
+ });
+ $('li.group_member').bind('ajax:success', function() {
+ return gl.utils.refreshCurrentPage();
+ });
+ showBlacklistType = function() {
+ if ($("input[name='blacklist_type']:checked").val() === 'file') {
+ $('.blacklist-file').show();
+ return $('.blacklist-raw').hide();
+ } else {
+ $('.blacklist-file').hide();
+ return $('.blacklist-raw').show();
+ }
+ };
+ $("input[name='blacklist_type']").click(showBlacklistType);
+ showBlacklistType();
+ }
- return Admin;
- })();
-}).call(window);
+ return Admin;
+})();
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
new file mode 100644
index 00000000000..38a8317dbd7
--- /dev/null
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -0,0 +1,35 @@
+class AjaxLoadingSpinner {
+ static init() {
+ const $elements = $('.js-ajax-loading-spinner');
+
+ $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+ }
+
+ static ajaxBeforeSend(e) {
+ e.target.setAttribute('disabled', '');
+ const iconElement = e.target.querySelector('i');
+ // get first fa- icon
+ const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first();
+ iconElement.dataset.icon = originalIcon;
+ AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+ $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ }
+
+ static ajaxComplete(e) {
+ e.target.removeAttribute('disabled');
+ const iconElement = e.target.querySelector('i');
+ AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+ $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+ }
+
+ static toggleLoadingIcon(iconElement) {
+ const classList = iconElement.classList;
+ classList.toggle(iconElement.dataset.icon);
+ classList.toggle('fa-spinner');
+ classList.toggle('fa-spin');
+ }
+}
+
+window.gl = window.gl || {};
+gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 86e0ad89431..e5f36c84987 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,150 +1,148 @@
/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
-(function() {
- var Api = {
- groupsPath: "/api/:version/groups.json",
- groupPath: "/api/:version/groups/:id.json",
- namespacesPath: "/api/:version/namespaces.json",
- groupProjectsPath: "/api/:version/groups/:id/projects.json",
- projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/:namespace_path/:project_path/labels",
- licensePath: "/api/:version/templates/licenses/:key",
- gitignorePath: "/api/:version/templates/gitignores/:key",
- gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
- dockerfilePath: "/api/:version/templates/dockerfiles/:key",
- issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
- group: function(group_id, callback) {
- var url = Api.buildUrl(Api.groupPath)
- .replace(':id', group_id);
- return $.ajax({
- url: url,
- dataType: "json"
- }).done(function(group) {
- return callback(group);
- });
- },
- // Return groups list. Filtered by query
- groups: function(query, options, callback) {
- var url = Api.buildUrl(Api.groupsPath);
- return $.ajax({
- url: url,
- data: $.extend({
- search: query,
- per_page: 20
- }, options),
- dataType: "json"
- }).done(function(groups) {
- return callback(groups);
- });
- },
- // Return namespaces list. Filtered by query
- namespaces: function(query, callback) {
- var url = Api.buildUrl(Api.namespacesPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: 20
- },
- dataType: "json"
- }).done(function(namespaces) {
- return callback(namespaces);
- });
- },
- // Return projects list. Filtered by query
- projects: function(query, order, callback) {
- var url = Api.buildUrl(Api.projectsPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- order_by: order,
- per_page: 20
- },
- dataType: "json"
- }).done(function(projects) {
- return callback(projects);
- });
- },
- newLabel: function(namespace_path, project_path, data, callback) {
- var url = Api.buildUrl(Api.labelsPath)
- .replace(':namespace_path', namespace_path)
- .replace(':project_path', project_path);
- return $.ajax({
- url: url,
- type: "POST",
- data: { 'label': data },
- dataType: "json"
- }).done(function(label) {
- return callback(label);
- }).error(function(message) {
- return callback(message.responseJSON);
- });
- },
- // Return group projects list. Filtered by query
- groupProjects: function(group_id, query, callback) {
- var url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', group_id);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: 20
- },
- dataType: "json"
- }).done(function(projects) {
- return callback(projects);
- });
- },
- // Return text for a specific license
- licenseText: function(key, data, callback) {
- var url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return $.ajax({
- url: url,
- data: data
- }).done(function(license) {
- return callback(license);
- });
- },
- gitignoreText: function(key, callback) {
- var url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return $.get(url, function(gitignore) {
- return callback(gitignore);
- });
- },
- gitlabCiYml: function(key, callback) {
- var url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return $.get(url, function(file) {
- return callback(file);
- });
- },
- dockerfileYml: function(key, callback) {
- var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- $.get(url, callback);
- },
- issueTemplate: function(namespacePath, projectPath, key, type, callback) {
- var url = Api.buildUrl(Api.issuableTemplatePath)
- .replace(':key', key)
- .replace(':type', type)
- .replace(':project_path', projectPath)
- .replace(':namespace_path', namespacePath);
- $.ajax({
- url: url,
- dataType: 'json'
- }).done(function(file) {
- callback(null, file);
- }).error(callback);
- },
- buildUrl: function(url) {
- if (gon.relative_url_root != null) {
- url = gon.relative_url_root + url;
- }
- return url.replace(':version', gon.api_version);
+var Api = {
+ groupsPath: "/api/:version/groups.json",
+ groupPath: "/api/:version/groups/:id.json",
+ namespacesPath: "/api/:version/namespaces.json",
+ groupProjectsPath: "/api/:version/groups/:id/projects.json",
+ projectsPath: "/api/:version/projects.json?simple=true",
+ labelsPath: "/:namespace_path/:project_path/labels",
+ licensePath: "/api/:version/templates/licenses/:key",
+ gitignorePath: "/api/:version/templates/gitignores/:key",
+ gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+ dockerfilePath: "/api/:version/templates/dockerfiles/:key",
+ issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+ group: function(group_id, callback) {
+ var url = Api.buildUrl(Api.groupPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ dataType: "json"
+ }).done(function(group) {
+ return callback(group);
+ });
+ },
+ // Return groups list. Filtered by query
+ groups: function(query, options, callback) {
+ var url = Api.buildUrl(Api.groupsPath);
+ return $.ajax({
+ url: url,
+ data: $.extend({
+ search: query,
+ per_page: 20
+ }, options),
+ dataType: "json"
+ }).done(function(groups) {
+ return callback(groups);
+ });
+ },
+ // Return namespaces list. Filtered by query
+ namespaces: function(query, callback) {
+ var url = Api.buildUrl(Api.namespacesPath);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(namespaces) {
+ return callback(namespaces);
+ });
+ },
+ // Return projects list. Filtered by query
+ projects: function(query, options, callback) {
+ var url = Api.buildUrl(Api.projectsPath);
+ return $.ajax({
+ url: url,
+ data: $.extend({
+ search: query,
+ per_page: 20,
+ membership: true
+ }, options),
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ newLabel: function(namespace_path, project_path, data, callback) {
+ var url = Api.buildUrl(Api.labelsPath)
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
+ return $.ajax({
+ url: url,
+ type: "POST",
+ data: { 'label': data },
+ dataType: "json"
+ }).done(function(label) {
+ return callback(label);
+ }).error(function(message) {
+ return callback(message.responseJSON);
+ });
+ },
+ // Return group projects list. Filtered by query
+ groupProjects: function(group_id, query, callback) {
+ var url = Api.buildUrl(Api.groupProjectsPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ // Return text for a specific license
+ licenseText: function(key, data, callback) {
+ var url = Api.buildUrl(Api.licensePath)
+ .replace(':key', key);
+ return $.ajax({
+ url: url,
+ data: data
+ }).done(function(license) {
+ return callback(license);
+ });
+ },
+ gitignoreText: function(key, callback) {
+ var url = Api.buildUrl(Api.gitignorePath)
+ .replace(':key', key);
+ return $.get(url, function(gitignore) {
+ return callback(gitignore);
+ });
+ },
+ gitlabCiYml: function(key, callback) {
+ var url = Api.buildUrl(Api.gitlabCiYmlPath)
+ .replace(':key', key);
+ return $.get(url, function(file) {
+ return callback(file);
+ });
+ },
+ dockerfileYml: function(key, callback) {
+ var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
+ $.get(url, callback);
+ },
+ issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+ var url = Api.buildUrl(Api.issuableTemplatePath)
+ .replace(':key', key)
+ .replace(':type', type)
+ .replace(':project_path', projectPath)
+ .replace(':namespace_path', namespacePath);
+ $.ajax({
+ url: url,
+ dataType: 'json'
+ }).done(function(file) {
+ callback(null, file);
+ }).error(callback);
+ },
+ buildUrl: function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root + url;
}
- };
+ return url.replace(':version', gon.api_version);
+ }
+};
- window.Api = Api;
-}).call(window);
+window.Api = Api;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
deleted file mode 100644
index 8e468faedbf..00000000000
--- a/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */
-/* global bp */
-/* global Cookies */
-/* global Flash */
-/* global ConfirmDangerModal */
-/* global AwardsHandler */
-/* global Aside */
-
-function requireAll(context) { return context.keys().map(context); }
-
-window.$ = window.jQuery = require('jquery');
-require('jquery-ui/ui/autocomplete');
-require('jquery-ui/ui/draggable');
-require('jquery-ui/ui/effect-highlight');
-require('jquery-ui/ui/sortable');
-require('jquery-ujs');
-require('vendor/jquery.endless-scroll');
-require('vendor/jquery.highlight');
-require('vendor/jquery.waitforimages');
-require('vendor/jquery.caret');
-require('vendor/jquery.atwho');
-require('vendor/jquery.scrollTo');
-window.Cookies = require('js-cookie');
-require('./autosave');
-require('bootstrap/js/affix');
-require('bootstrap/js/alert');
-require('bootstrap/js/button');
-require('bootstrap/js/collapse');
-require('bootstrap/js/dropdown');
-require('bootstrap/js/modal');
-require('bootstrap/js/scrollspy');
-require('bootstrap/js/tab');
-require('bootstrap/js/transition');
-require('bootstrap/js/tooltip');
-require('bootstrap/js/popover');
-require('select2/select2.js');
-window.Pikaday = require('pikaday');
-window._ = require('underscore');
-window.Dropzone = require('dropzone');
-window.Sortable = require('vendor/Sortable');
-require('mousetrap');
-require('mousetrap/plugins/pause/mousetrap-pause');
-require('./shortcuts');
-require('./shortcuts_navigation');
-require('./shortcuts_dashboard_navigation');
-require('./shortcuts_issuable');
-require('./shortcuts_network');
-require('vendor/jquery.nicescroll');
-requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
-require('vendor/fuzzaldrin-plus');
-require('es6-promise').polyfill();
-
-(function () {
- document.addEventListener('beforeunload', function () {
- // Unbind scroll events
- $(document).off('scroll');
- // Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
- });
-
- window.addEventListener('hashchange', gl.utils.handleLocationHash);
- window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- gl.utils.handleLocationHash();
- }, false);
-
- $(function () {
- var $body = $('body');
- var $document = $(document);
- var $window = $(window);
- var $sidebarGutterToggle = $('.js-sidebar-toggle');
- var $flash = $('.flash-container');
- var bootstrapBreakpoint = bp.getBreakpointSize();
- var fitSidebarForSize;
-
- // Set the default path for all cookies to GitLab's root directory
- Cookies.defaults.path = gon.relative_url_root || '/';
-
- // `hashchange` is not triggered when link target is already in window.location
- $body.on('click', 'a[href^="#"]', function() {
- var href = this.getAttribute('href');
- if (href.substr(1) === gl.utils.getLocationHash()) {
- setTimeout(gl.utils.handleLocationHash, 1);
- }
- });
-
- // prevent default action for disabled buttons
- $('.btn').click(function(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
- }
- });
-
- $('.js-select-on-focus').on('focusin', function () {
- return $(this).select().one('mouseup', function (e) {
- return e.preventDefault();
- });
- // Click a .js-select-on-focus field, select the contents
- // Prevent a mouseup event from deselecting the input
- });
- $('.remove-row').bind('ajax:success', function () {
- $(this).tooltip('destroy')
- .closest('li')
- .fadeOut();
- });
- $('.js-remove-tr').bind('ajax:before', function () {
- return $(this).hide();
- });
- $('.js-remove-tr').bind('ajax:success', function () {
- return $(this).closest('tr').fadeOut();
- });
- $('select.select2').select2({
- width: 'resolve',
- // Initialize select2 selects
- dropdownAutoWidth: true
- });
- $('.js-select2').bind('select2-close', function () {
- return setTimeout((function () {
- $('.select2-container-active').removeClass('select2-container-active');
- return $(':focus').blur();
- }), 1);
- // Close select2 on escape
- });
- // Initialize tooltips
- $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
- $body.tooltip({
- selector: '.has-tooltip, [data-toggle="tooltip"]',
- placement: function (_, el) {
- return $(el).data('placement') || 'bottom';
- }
- });
- $('.trigger-submit').on('change', function () {
- return $(this).parents('form').submit();
- // Form submitter
- });
- gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
- // Flash
- if ($flash.length > 0) {
- $flash.click(function () {
- return $(this).fadeOut();
- });
- $flash.show();
- }
- // Disable form buttons while a form is submitting
- $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
- var buttons;
- buttons = $('[type="submit"]', this);
- switch (e.type) {
- case 'ajax:beforeSend':
- case 'submit':
- return buttons.disable();
- default:
- return buttons.enable();
- }
- });
- $(document).ajaxError(function (e, xhrObj) {
- var ref = xhrObj.status;
- if (xhrObj.status === 401) {
- return new Flash('You need to be logged in.', 'alert');
- } else if (ref === 404 || ref === 500) {
- return new Flash('Something went wrong on our end.', 'alert');
- }
- });
- $('.account-box').hover(function () {
- // Show/Hide the profile menu when hovering the account box
- return $(this).toggleClass('hover');
- });
- $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
- var $container;
- $container = $(this).parent();
- $container.next('table').show();
- return $container.remove();
- // Commit show suppressed diff
- });
- $('.navbar-toggle').on('click', function () {
- $('.header-content .title').toggle();
- $('.header-content .header-logo').toggle();
- $('.header-content .navbar-collapse').toggle();
- return $('.navbar-toggle').toggleClass('active');
- });
- // Show/hide comments on diff
- $body.on('click', '.js-toggle-diff-comments', function (e) {
- var $this = $(this);
- var notesHolders = $this.closest('.diff-file').find('.notes_holder');
- $this.toggleClass('active');
- if ($this.hasClass('active')) {
- notesHolders.show().find('.hide').show();
- } else {
- notesHolders.hide();
- }
- $this.trigger('blur');
- return e.preventDefault();
- });
- $document.off('click', '.js-confirm-danger');
- $document.on('click', '.js-confirm-danger', function (e) {
- var btn = $(e.target);
- var form = btn.closest('form');
- var text = btn.data('confirm-danger-message');
- e.preventDefault();
- return new ConfirmDangerModal(form, text);
- });
- $('input[type="search"]').each(function () {
- var $this = $(this);
- $this.attr('value', $this.val());
- });
- $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
- var $this;
- $this = $(this);
- return $this.attr('value', $this.val());
- });
- $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
- var $gutterIcon;
- if (breakpoint === 'sm' || breakpoint === 'xs') {
- $gutterIcon = $sidebarGutterToggle.find('i');
- if ($gutterIcon.hasClass('fa-angle-double-right')) {
- return $sidebarGutterToggle.trigger('click');
- }
- }
- });
- fitSidebarForSize = function () {
- var oldBootstrapBreakpoint;
- oldBootstrapBreakpoint = bootstrapBreakpoint;
- bootstrapBreakpoint = bp.getBreakpointSize();
- if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
- return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
- }
- };
- $window.off('resize.app').on('resize.app', function () {
- return fitSidebarForSize();
- });
- gl.awardsHandler = new AwardsHandler();
- new Aside();
-
- gl.utils.initTimeagoTimeout();
- });
-}).call(window);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 448e6e2cc78..88756884d16 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,25 +1,24 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
- this.Aside = (function() {
- function Aside() {
- $(document).off("click", "a.show-aside");
- $(document).on("click", 'a.show-aside', function(e) {
- var btn, icon;
- e.preventDefault();
- btn = $(e.currentTarget);
- icon = btn.find('i');
- if (icon.hasClass('fa-angle-left')) {
- btn.parent().find('section').hide();
- btn.parent().find('aside').fadeIn();
- return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
- } else {
- btn.parent().find('aside').hide();
- btn.parent().find('section').fadeIn();
- return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
- }
- });
- }
- return Aside;
- })();
-}).call(window);
+window.Aside = (function() {
+ function Aside() {
+ $(document).off("click", "a.show-aside");
+ $(document).on("click", 'a.show-aside', function(e) {
+ var btn, icon;
+ e.preventDefault();
+ btn = $(e.currentTarget);
+ icon = btn.find('i');
+ if (icon.hasClass('fa-angle-left')) {
+ btn.parent().find('section').hide();
+ btn.parent().find('aside').fadeIn();
+ return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
+ } else {
+ btn.parent().find('aside').hide();
+ btn.parent().find('section').fadeIn();
+ return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
+ }
+ });
+ }
+
+ return Aside;
+})();
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index e55405135fb..8630b18a73f 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,62 +1,61 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
-(function() {
- this.Autosave = (function() {
- function Autosave(field, key) {
- this.field = field;
- if (key.join != null) {
- key = key.join("/");
- }
- this.key = "autosave/" + key;
- this.field.data("autosave", this);
- this.restore();
- this.field.on("input", (function(_this) {
- return function() {
- return _this.save();
- };
- })(this));
- }
- Autosave.prototype.restore = function() {
- var e, text;
- if (window.localStorage == null) {
- return;
- }
- try {
- text = window.localStorage.getItem(this.key);
- } catch (error) {
- e = error;
- return;
- }
- if ((text != null ? text.length : void 0) > 0) {
- this.field.val(text);
- }
- return this.field.trigger("input");
- };
+window.Autosave = (function() {
+ function Autosave(field, key) {
+ this.field = field;
+ if (key.join != null) {
+ key = key.join("/");
+ }
+ this.key = "autosave/" + key;
+ this.field.data("autosave", this);
+ this.restore();
+ this.field.on("input", (function(_this) {
+ return function() {
+ return _this.save();
+ };
+ })(this));
+ }
- Autosave.prototype.save = function() {
- var text;
- if (window.localStorage == null) {
- return;
- }
- text = this.field.val();
- if ((text != null ? text.length : void 0) > 0) {
- try {
- return window.localStorage.setItem(this.key, text);
- } catch (error) {}
- } else {
- return this.reset();
- }
- };
+ Autosave.prototype.restore = function() {
+ var e, text;
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ text = window.localStorage.getItem(this.key);
+ } catch (error) {
+ e = error;
+ return;
+ }
+ if ((text != null ? text.length : void 0) > 0) {
+ this.field.val(text);
+ }
+ return this.field.trigger("input");
+ };
- Autosave.prototype.reset = function() {
- if (window.localStorage == null) {
- return;
- }
+ Autosave.prototype.save = function() {
+ var text;
+ if (window.localStorage == null) {
+ return;
+ }
+ text = this.field.val();
+ if ((text != null ? text.length : void 0) > 0) {
try {
- return window.localStorage.removeItem(this.key);
+ return window.localStorage.setItem(this.key, text);
} catch (error) {}
- };
+ } else {
+ return this.reset();
+ }
+ };
+
+ Autosave.prototype.reset = function() {
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ return window.localStorage.removeItem(this.key);
+ } catch (error) {}
+ };
- return Autosave;
- })();
-}).call(window);
+ return Autosave;
+})();
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index a4ccb30e447..9349918f7a0 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,380 +1,518 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */
/* global Cookies */
-var emojiAliases = require('emoji-aliases');
-
-(function() {
- this.AwardsHandler = (function() {
- var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
- function AwardsHandler() {
- this.aliases = emojiAliases;
- $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
- return function(e) {
- e.stopPropagation();
- e.preventDefault();
- return _this.showEmojiMenu($(e.currentTarget));
- };
- })(this));
- $('html').on('click', function(e) {
- var $target;
- $target = $(e.target);
- if (!$target.closest('.emoji-menu-content').length) {
- $('.js-awards-block.current').removeClass('current');
- }
- if (!$target.closest('.emoji-menu').length) {
- if ($('.emoji-menu').is(':visible')) {
- $('.js-add-award.is-active').removeClass('is-active');
- return $('.emoji-menu').removeClass('is-visible');
- }
- }
- });
- $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) {
- return function(e) {
- var $target, emoji;
- e.preventDefault();
- $target = $(e.currentTarget);
- emoji = $target.find('.icon').data('emoji');
- $target.closest('.js-awards-block').addClass('current');
- return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji);
- };
- })(this));
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from './behaviors/gl_emoji';
+import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
+
+const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
+const requestAnimationFrame = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.setTimeout;
+
+const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
+
+let categoryMap = null;
+
+const categoryLabelMap = {
+ activity: 'Activity',
+ people: 'People',
+ nature: 'Nature',
+ food: 'Food',
+ travel: 'Travel',
+ objects: 'Objects',
+ symbols: 'Symbols',
+ flags: 'Flags',
+};
+
+function buildCategoryMap() {
+ return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
+ const emojiInfo = emojiMap[emojiNameKey];
+ if (currentCategoryMap[emojiInfo.category]) {
+ currentCategoryMap[emojiInfo.category].push(emojiNameKey);
}
- AwardsHandler.prototype.showEmojiMenu = function($addBtn) {
- var $holder, $menu, url;
- $menu = $('.emoji-menu');
- if ($addBtn.hasClass('js-note-emoji')) {
- $addBtn.closest('.note').find('.js-awards-block').addClass('current');
- } else {
- $addBtn.closest('.js-awards-block').addClass('current');
- }
- if ($menu.length) {
- $holder = $addBtn.closest('.js-award-holder');
- if ($menu.is('.is-visible')) {
- $addBtn.removeClass('is-active');
- $menu.removeClass('is-visible');
- return $('#emoji_search').blur();
- } else {
- $addBtn.addClass('is-active');
- this.positionMenu($menu, $addBtn);
- $menu.addClass('is-visible');
- return $('#emoji_search').focus();
- }
- } else {
- $addBtn.addClass('is-loading is-active');
- url = this.getAwardMenuUrl();
- return this.createEmojiMenu(url, (function(_this) {
- return function() {
- $addBtn.removeClass('is-loading');
- $menu = $('.emoji-menu');
- _this.positionMenu($menu, $addBtn);
- if (!_this.frequentEmojiBlockRendered) {
- _this.renderFrequentlyUsedBlock();
- }
- return setTimeout(function() {
- $menu.addClass('is-visible');
- $('#emoji_search').focus();
- return _this.setupSearch();
- }, 200);
- };
- })(this));
- }
- };
-
- AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) {
- return $.get(awardMenuUrl, function(response) {
- $('body').append(response);
- return callback();
+ return currentCategoryMap;
+ }, {
+ activity: [],
+ people: [],
+ nature: [],
+ food: [],
+ travel: [],
+ objects: [],
+ symbols: [],
+ flags: [],
+ });
+}
+
+function renderCategory(name, emojiList, opts = {}) {
+ return `
+ <h5 class="emoji-menu-title">
+ ${name}
+ </h5>
+ <ul class="clearfix emoji-menu-list ${opts.menuListClass}">
+ ${emojiList.map(emojiName => `
+ <li class="emoji-menu-list-item">
+ <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
+ ${glEmojiTag(emojiName, {
+ sprite: true,
+ })}
+ </button>
+ </li>
+ `).join('\n')}
+ </ul>
+ `;
+}
+
+function AwardsHandler() {
+ this.eventListeners = [];
+ this.aliases = emojiAliases;
+ // If the user shows intent let's pre-build the menu
+ this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
+ const $menu = $('.emoji-menu');
+ if ($menu.length === 0) {
+ requestAnimationFrame(() => {
+ this.createEmojiMenu();
});
- };
-
- AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
- var css, position;
- position = $addBtn.data('position');
- // The menu could potentially be off-screen or in a hidden overflow element
- // So we position the element absolute in the body
- css = {
- top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
- };
- if (position === 'right') {
- css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
- $menu.addClass('is-aligned-right');
- } else {
- css.left = ($addBtn.offset().left) + "px";
- $menu.removeClass('is-aligned-right');
- }
- return $menu.css(css);
- };
-
- AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) {
- if (checkMutuality == null) {
- checkMutuality = true;
- }
- emoji = this.normilizeEmojiName(emoji);
- this.postEmoji(awardUrl, emoji, (function(_this) {
- return function() {
- _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality);
- return typeof callback === "function" ? callback() : void 0;
- };
- })(this));
- return $('.emoji-menu').removeClass('is-visible');
- };
-
- AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) {
- var $emojiButton, counter;
- if (checkForMutuality == null) {
- checkForMutuality = true;
- }
- if (checkForMutuality) {
- this.checkMutuality(votesBlock, emoji);
- }
- this.addEmojiToFrequentlyUsedList(emoji);
- emoji = this.normilizeEmojiName(emoji);
- $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent();
- if ($emojiButton.length > 0) {
- if (this.isActive($emojiButton)) {
- return this.decrementCounter($emojiButton, emoji);
- } else {
- counter = $emojiButton.find('.js-counter');
- counter.text(parseInt(counter.text(), 10) + 1);
- $emojiButton.addClass('active');
- this.addYouToUserList(votesBlock, emoji);
- return this.animateEmoji($emojiButton);
- }
- } else {
- votesBlock.removeClass('hidden');
- return this.createEmoji(votesBlock, emoji);
- }
- };
-
- AwardsHandler.prototype.getVotesBlock = function() {
- var currentBlock;
- currentBlock = $('.js-awards-block.current');
- if (currentBlock.length) {
- return currentBlock;
- } else {
- return $('.js-awards-block').eq(0);
- }
- };
-
- AwardsHandler.prototype.getAwardUrl = function() {
- return this.getVotesBlock().data('award-url');
- };
-
- AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) {
- var $emojiButton, awardUrl, isAlreadyVoted, mutualVote;
- awardUrl = this.getAwardUrl();
- if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
- mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
- $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent();
- isAlreadyVoted = $emojiButton.hasClass('active');
- if (isAlreadyVoted) {
- this.addAward(votesBlock, awardUrl, mutualVote, false);
- }
- }
- };
-
- AwardsHandler.prototype.isActive = function($emojiButton) {
- return $emojiButton.hasClass('active');
- };
-
- AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) {
- var counter, counterNumber;
- counter = $('.js-counter', $emojiButton);
- counterNumber = parseInt(counter.text(), 10);
- if (counterNumber > 1) {
- counter.text(counterNumber - 1);
- this.removeYouFromUserList($emojiButton, emoji);
- } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
- $emojiButton.tooltip('destroy');
- counter.text('0');
- this.removeYouFromUserList($emojiButton, emoji);
- if ($emojiButton.parents('.note').length) {
- this.removeEmoji($emojiButton);
- }
- } else {
- this.removeEmoji($emojiButton);
- }
- return $emojiButton.removeClass('active');
- };
-
- AwardsHandler.prototype.removeEmoji = function($emojiButton) {
- var $votesBlock;
- $emojiButton.tooltip('destroy');
- $emojiButton.remove();
- $votesBlock = this.getVotesBlock();
- if ($votesBlock.find('.js-emoji-btn').length === 0) {
- return $votesBlock.addClass('hidden');
- }
- };
-
- AwardsHandler.prototype.getAwardTooltip = function($awardBlock) {
- return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
- };
-
- AwardsHandler.prototype.toSentence = function(list) {
- if (list.length <= 2) {
- return list.join(' and ');
- }
- else {
- return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
+ }
+ // Prebuild the categoryMap
+ categoryMap = categoryMap || buildCategoryMap();
+ });
+ this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.showEmojiMenu($(e.currentTarget));
+ });
+
+ this.registerEventListener('on', $('html'), 'click', (e) => {
+ const $target = $(e.target);
+ if (!$target.closest('.emoji-menu-content').length) {
+ $('.js-awards-block.current').removeClass('current');
+ }
+ if (!$target.closest('.emoji-menu').length) {
+ if ($('.emoji-menu').is(':visible')) {
+ $('.js-add-award.is-active').removeClass('is-active');
+ $('.emoji-menu').removeClass('is-visible');
}
- };
-
- AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
- var authors, awardBlock, newAuthors, originalTitle;
- awardBlock = $emojiButton;
- originalTitle = this.getAwardTooltip(awardBlock);
- authors = originalTitle.split(FROM_SENTENCE_REGEX);
- authors.splice(authors.indexOf('You'), 1);
- return awardBlock
- .closest('.js-emoji-btn')
- .removeData('title')
- .removeAttr('data-title')
- .removeAttr('data-original-title')
- .attr('title', this.toSentence(authors))
- .tooltip('fixTitle');
- };
-
- AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
- var awardBlock, origTitle, users;
- awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
- origTitle = this.getAwardTooltip(awardBlock);
- users = [];
- if (origTitle) {
- users = origTitle.trim().split(FROM_SENTENCE_REGEX);
+ }
+ });
+ this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ const $glEmojiElement = $target.find('gl-emoji');
+ const $spriteIconElement = $target.find('.icon');
+ const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
+ $target.closest('.js-awards-block').addClass('current');
+ return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
+ });
+}
+
+AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) {
+ element[method].call(element, ...args);
+ this.eventListeners.push({
+ element,
+ args,
+ });
+};
+
+AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
+ if ($addBtn.hasClass('js-note-emoji')) {
+ $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+ } else {
+ $addBtn.closest('.js-awards-block').addClass('current');
+ }
+
+ const $menu = $('.emoji-menu');
+ if ($menu.length) {
+ if ($menu.is('.is-visible')) {
+ $addBtn.removeClass('is-active');
+ $menu.removeClass('is-visible');
+ $('#emoji_search').blur();
+ } else {
+ $addBtn.addClass('is-active');
+ this.positionMenu($menu, $addBtn);
+ $menu.addClass('is-visible');
+ $('#emoji_search').focus();
+ }
+ } else {
+ $addBtn.addClass('is-loading is-active');
+ this.createEmojiMenu(() => {
+ const $createdMenu = $('.emoji-menu');
+ $addBtn.removeClass('is-loading');
+ this.positionMenu($createdMenu, $addBtn);
+ return setTimeout(() => {
+ $createdMenu.addClass('is-visible');
+ $('#emoji_search').focus();
+ }, 200);
+ });
+ }
+};
+
+// Create the emoji menu with the first category of emojis.
+// Then render the remaining categories of emojis one by one to avoid jank.
+AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
+ if (this.isCreatingEmojiMenu) {
+ return;
+ }
+ this.isCreatingEmojiMenu = true;
+
+ // Render the first category
+ categoryMap = categoryMap || buildCategoryMap();
+ const categoryNameKey = Object.keys(categoryMap)[0];
+ const emojisInCategory = categoryMap[categoryNameKey];
+ const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
+
+ // Render the frequently used
+ const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
+ let frequentlyUsedCatgegory = '';
+ if (frequentlyUsedEmojis.length > 0) {
+ frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
+ menuListClass: 'frequent-emojis',
+ });
+ }
+
+ const emojiMenuMarkup = `
+ <div class="emoji-menu">
+ <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" />
+
+ <div class="emoji-menu-content">
+ ${frequentlyUsedCatgegory}
+ ${firstCategory}
+ </div>
+ </div>
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
+
+ this.addRemainingEmojiMenuCategories();
+ this.setupSearch();
+ if (callback) {
+ callback();
+ }
+};
+
+AwardsHandler
+ .prototype
+ .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() {
+ if (this.isAddingRemainingEmojiMenuCategories) {
+ return;
+ }
+ this.isAddingRemainingEmojiMenuCategories = true;
+
+ categoryMap = categoryMap || buildCategoryMap();
+
+ // Avoid the jank and render the remaining categories separately
+ // This will take more time, but makes UI more responsive
+ const menu = document.querySelector('.emoji-menu');
+ const emojiContentElement = menu.querySelector('.emoji-menu-content');
+ const remainingCategories = Object.keys(categoryMap).slice(1);
+ const allCategoriesAddedPromise = remainingCategories.reduce(
+ (promiseChain, categoryNameKey) =>
+ promiseChain.then(() =>
+ new Promise((resolve) => {
+ const emojisInCategory = categoryMap[categoryNameKey];
+ const categoryMarkup = renderCategory(
+ categoryLabelMap[categoryNameKey],
+ emojisInCategory,
+ );
+ requestAnimationFrame(() => {
+ emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
+ resolve();
+ });
+ }),
+ ),
+ Promise.resolve(),
+ );
+
+ allCategoriesAddedPromise.then(() => {
+ // Used for tests
+ // We check for the menu in case it was destroyed in the meantime
+ if (menu) {
+ menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
}
- users.unshift('You');
- return awardBlock
- .attr('title', this.toSentence(users))
- .tooltip('fixTitle');
- };
-
- AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
- var $emojiButton, buttonHtml, emojiCssClass;
- emojiCssClass = this.resolveNameToCssClass(emoji);
- buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
- $emojiButton = $(buttonHtml);
- $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
+ });
+ };
+
+AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) {
+ const position = $addBtn.data('position');
+ // The menu could potentially be off-screen or in a hidden overflow element
+ // So we position the element absolute in the body
+ const css = {
+ top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
+ };
+ if (position === 'right') {
+ css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
+ $menu.addClass('is-aligned-right');
+ } else {
+ css.left = `${$addBtn.offset().left}px`;
+ $menu.removeClass('is-aligned-right');
+ }
+ return $menu.css(css);
+};
+
+AwardsHandler.prototype.addAward = function addAward(
+ votesBlock,
+ awardUrl,
+ emoji,
+ checkMutuality,
+ callback,
+) {
+ const normalizedEmoji = this.normalizeEmojiName(emoji);
+ this.postEmoji(awardUrl, normalizedEmoji, () => {
+ this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
+ return typeof callback === 'function' ? callback() : undefined;
+ });
+ return $('.emoji-menu').removeClass('is-visible');
+};
+
+AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
+ votesBlock,
+ emoji,
+ checkForMutuality,
+) {
+ if (checkForMutuality || checkForMutuality === null) {
+ this.checkMutuality(votesBlock, emoji);
+ }
+ this.addEmojiToFrequentlyUsedList(emoji);
+ const normalizedEmoji = this.normalizeEmojiName(emoji);
+ const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
+ if ($emojiButton.length > 0) {
+ if (this.isActive($emojiButton)) {
+ this.decrementCounter($emojiButton, normalizedEmoji);
+ } else {
+ const counter = $emojiButton.find('.js-counter');
+ counter.text(parseInt(counter.text(), 10) + 1);
+ $emojiButton.addClass('active');
+ this.addYouToUserList(votesBlock, normalizedEmoji);
this.animateEmoji($emojiButton);
- $('.award-control').tooltip();
- return votesBlock.removeClass('current');
- };
-
- AwardsHandler.prototype.animateEmoji = function($emoji) {
- var className = 'pulse animated once short';
- $emoji.addClass(className);
+ }
+ } else {
+ votesBlock.removeClass('hidden');
+ this.createEmoji(votesBlock, normalizedEmoji);
+ }
+};
+
+AwardsHandler.prototype.getVotesBlock = function getVotesBlock() {
+ const currentBlock = $('.js-awards-block.current');
+ let resultantVotesBlock = currentBlock;
+ if (currentBlock.length === 0) {
+ resultantVotesBlock = $('.js-awards-block').eq(0);
+ }
+
+ return resultantVotesBlock;
+};
+
+AwardsHandler.prototype.getAwardUrl = function getAwardUrl() {
+ return this.getVotesBlock().data('award-url');
+};
+
+AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) {
+ const awardUrl = this.getAwardUrl();
+ if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+ const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
+ const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
+ const isAlreadyVoted = $emojiButton.hasClass('active');
+ if (isAlreadyVoted) {
+ this.addAward(votesBlock, awardUrl, mutualVote, false);
+ }
+ }
+};
+
+AwardsHandler.prototype.isActive = function isActive($emojiButton) {
+ return $emojiButton.hasClass('active');
+};
+
+AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
+ const counter = $('.js-counter', $emojiButton);
+ const counterNumber = parseInt(counter.text(), 10);
+ if (counterNumber > 1) {
+ counter.text(counterNumber - 1);
+ this.removeYouFromUserList($emojiButton);
+ } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+ $emojiButton.tooltip('destroy');
+ counter.text('0');
+ this.removeYouFromUserList($emojiButton);
+ if ($emojiButton.parents('.note').length) {
+ this.removeEmoji($emojiButton);
+ }
+ } else {
+ this.removeEmoji($emojiButton);
+ }
+ return $emojiButton.removeClass('active');
+};
+
+AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) {
+ $emojiButton.tooltip('destroy');
+ $emojiButton.remove();
+ const $votesBlock = this.getVotesBlock();
+ if ($votesBlock.find('.js-emoji-btn').length === 0) {
+ $votesBlock.addClass('hidden');
+ }
+};
+
+AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) {
+ return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
+};
+
+AwardsHandler.prototype.toSentence = function toSentence(list) {
+ let sentence;
+ if (list.length <= 2) {
+ sentence = list.join(' and ');
+ } else {
+ sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
+ }
+
+ return sentence;
+};
+
+AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) {
+ const awardBlock = $emojiButton;
+ const originalTitle = this.getAwardTooltip(awardBlock);
+ const authors = originalTitle.split(FROM_SENTENCE_REGEX);
+ authors.splice(authors.indexOf('You'), 1);
+ return awardBlock
+ .closest('.js-emoji-btn')
+ .removeData('title')
+ .removeAttr('data-title')
+ .removeAttr('data-original-title')
+ .attr('title', this.toSentence(authors))
+ .tooltip('fixTitle');
+};
+
+AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) {
+ const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
+ const origTitle = this.getAwardTooltip(awardBlock);
+ let users = [];
+ if (origTitle) {
+ users = origTitle.trim().split(FROM_SENTENCE_REGEX);
+ }
+ users.unshift('You');
+ return awardBlock
+ .attr('title', this.toSentence(users))
+ .tooltip('fixTitle');
+};
+
+AwardsHandler
+ .prototype
+ .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) {
+ const buttonHtml = `
+ <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
+ ${glEmojiTag(emojiName)}
+ <span class="award-control-text js-counter">1</span>
+ </button>
+ `;
+ const $emojiButton = $(buttonHtml);
+ $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
+ this.animateEmoji($emojiButton);
+ $('.award-control').tooltip();
+ votesBlock.removeClass('current');
+ };
+
+AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) {
+ const className = 'pulse animated once short';
+ $emoji.addClass(className);
+
+ this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
+ $(e.currentTarget).removeClass(className);
+ });
+};
+
+AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
+ if ($('.emoji-menu').length) {
+ this.createAwardButtonForVotesBlock(votesBlock, emoji);
+ }
+ this.createEmojiMenu(() => {
+ this.createAwardButtonForVotesBlock(votesBlock, emoji);
+ });
+};
+
+AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) {
+ return $.post(awardUrl, {
+ name: emoji,
+ }, (data) => {
+ if (data.ok) {
+ callback();
+ }
+ });
+};
+
+AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
+ return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
+};
+
+AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
+ const options = {
+ scrollTop: $('.awards').offset().top - 110,
+ };
+ return $('body, html').animate(options, 200);
+};
+
+AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) {
+ return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
+};
+
+AwardsHandler
+ .prototype
+ .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
+ if (isEmojiNameValid(emoji)) {
+ this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
+ Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
+ }
+ };
- $emoji.on('webkitAnimationEnd animationEnd', function() {
- $(this).removeClass(className);
- });
- };
+AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
+ return this.frequentlyUsedEmojis || (() => {
+ const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
+ this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
+ inputName => isEmojiNameValid(inputName),
+ );
- AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
- if ($('.emoji-menu').length) {
- return this.createEmoji_(votesBlock, emoji);
- }
- return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) {
- return function() {
- return _this.createEmoji_(votesBlock, emoji);
- };
- })(this));
- };
-
- AwardsHandler.prototype.getAwardMenuUrl = function() {
- return gon.award_menu_url;
- };
-
- AwardsHandler.prototype.resolveNameToCssClass = function(emoji) {
- var emojiIcon, unicodeName;
- emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']");
- if (emojiIcon.length > 0) {
- unicodeName = emojiIcon.data('unicode-name');
- } else {
- // Find by alias
- unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
- }
- return "emoji-" + unicodeName;
- };
-
- AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) {
- return $.post(awardUrl, {
- name: emoji
- }, function(data) {
- if (data.ok) {
- return callback();
- }
- });
- };
-
- AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) {
- return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']");
- };
-
- AwardsHandler.prototype.scrollToAwards = function() {
- var options;
- options = {
- scrollTop: $('.awards').offset().top - 110
- };
- return $('body, html').animate(options, 200);
- };
-
- AwardsHandler.prototype.normilizeEmojiName = function(emoji) {
- return this.aliases[emoji] || emoji;
- };
-
- AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) {
- var frequentlyUsedEmojis;
- frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
- frequentlyUsedEmojis.push(emoji);
- Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
- };
-
- AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
- var frequentlyUsedEmojis;
- frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
- return _.compact(_.uniq(frequentlyUsedEmojis));
- };
-
- AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
- var emoji, frequentlyUsedEmojis, i, len, ul;
- if (Cookies.get('frequently_used_emojis')) {
- frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
- ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
- for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
- emoji = frequentlyUsedEmojis[i];
- $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
- }
- $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used'));
- }
- return this.frequentEmojiBlockRendered = true;
- };
-
- AwardsHandler.prototype.setupSearch = function() {
- return $('input.emoji-search').on('keyup', (function(_this) {
- return function(ev) {
- var found_emojis, h5, term, ul;
- term = $(ev.target).val();
- // Clean previous search results
- $('ul.emoji-menu-search, h5.emoji-search').remove();
- if (term) {
- // Generate a search result block
- h5 = $('<h5 class="emoji-search" />').text('Search results');
- found_emojis = _this.searchEmojis(term).show();
- ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
- $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
- return $('.emoji-menu-content').append(h5).append(ul);
- } else {
- return $('.emoji-menu-content').children().show();
- }
- };
- })(this));
- };
-
- AwardsHandler.prototype.searchEmojis = function(term) {
- return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone();
- };
-
- return AwardsHandler;
+ return this.frequentlyUsedEmojis;
})();
-}).call(window);
+};
+
+AwardsHandler.prototype.setupSearch = function setupSearch() {
+ this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
+ const term = $(e.target).val().trim();
+ // Clean previous search results
+ $('ul.emoji-menu-search, h5.emoji-search').remove();
+ if (term.length > 0) {
+ // Generate a search result block
+ const h5 = $('<h5 class="emoji-search" />').text('Search results');
+ const foundEmojis = this.searchEmojis(term).show();
+ const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
+ $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
+ $('.emoji-menu-content').append(h5).append(ul);
+ } else {
+ $('.emoji-menu-content').children().show();
+ }
+ });
+};
+
+AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
+ const safeTerm = term.toLowerCase();
+
+ const namesMatchingAlias = [];
+ Object.keys(emojiAliases).forEach((alias) => {
+ if (alias.indexOf(safeTerm) >= 0) {
+ namesMatchingAlias.push(emojiAliases[alias]);
+ }
+ });
+ const $matchingElements = namesMatchingAlias.concat(safeTerm)
+ .reduce(
+ ($result, searchTerm) =>
+ $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
+ $([]),
+ );
+ return $matchingElements.closest('li').clone();
+};
+
+AwardsHandler.prototype.destroy = function destroy() {
+ this.eventListeners.forEach((entry) => {
+ entry.element.off.call(entry.element, ...entry.args);
+ });
+ $('.emoji-menu').remove();
+};
+
+export default AwardsHandler;
diff --git a/app/assets/javascripts/behaviors/bind_in_out.js b/app/assets/javascripts/behaviors/bind_in_out.js
new file mode 100644
index 00000000000..886f127b06b
--- /dev/null
+++ b/app/assets/javascripts/behaviors/bind_in_out.js
@@ -0,0 +1,47 @@
+class BindInOut {
+ constructor(bindIn, bindOut) {
+ this.in = bindIn;
+ this.out = bindOut;
+
+ this.eventWrapper = {};
+ this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change';
+ }
+
+ addEvents() {
+ this.eventWrapper.updateOut = this.updateOut.bind(this);
+
+ this.in.addEventListener(this.eventType, this.eventWrapper.updateOut);
+
+ return this;
+ }
+
+ updateOut() {
+ this.out.textContent = this.in.value;
+
+ return this;
+ }
+
+ removeEvents() {
+ this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut);
+
+ return this;
+ }
+
+ static initAll() {
+ const ins = document.querySelectorAll('*[data-bind-in]');
+
+ return [].map.call(ins, anIn => BindInOut.init(anIn));
+ }
+
+ static init(anIn, anOut) {
+ const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`);
+
+ if (!out) return null;
+
+ const bindInOut = new BindInOut(anIn, out);
+
+ return bindInOut.addEvents().updateOut();
+ }
+}
+
+export default BindInOut;
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
new file mode 100644
index 00000000000..19a607309e4
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -0,0 +1,116 @@
+import installCustomElements from 'document-register-element';
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
+import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
+
+installCustomElements(window);
+
+const generatedUnicodeSupportMap = getUnicodeSupportMap();
+
+function emojiImageTag(name, src) {
+ return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
+}
+
+function assembleFallbackImageSrc(inputName) {
+ let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+ let emojiInfo = emojiMap[name];
+ // Fallback to question mark for unknown emojis
+ if (!emojiInfo) {
+ name = 'grey_question';
+ emojiInfo = emojiMap[name];
+ }
+ const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
+
+ return fallbackImageSrc;
+}
+const glEmojiTagDefaults = {
+ sprite: false,
+ forceFallback: false,
+};
+function glEmojiTag(inputName, options) {
+ const opts = Object.assign({}, glEmojiTagDefaults, options);
+ let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+ let emojiInfo = emojiMap[name];
+ // Fallback to question mark for unknown emojis
+ if (!emojiInfo) {
+ name = 'grey_question';
+ emojiInfo = emojiMap[name];
+ }
+
+ const fallbackImageSrc = assembleFallbackImageSrc(name);
+ const fallbackSpriteClass = `emoji-${name}`;
+
+ const classList = [];
+ if (opts.forceFallback && opts.sprite) {
+ classList.push('emoji-icon');
+ classList.push(fallbackSpriteClass);
+ }
+ const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
+ const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+ let contents = emojiInfo.moji;
+ if (opts.forceFallback && !opts.sprite) {
+ contents = emojiImageTag(name, fallbackImageSrc);
+ }
+
+ return `
+ <gl-emoji
+ ${classAttribute}
+ data-name="${name}"
+ data-fallback-src="${fallbackImageSrc}"
+ ${fallbackSpriteAttribute}
+ data-unicode-version="${emojiInfo.unicodeVersion}"
+ >
+ ${contents}
+ </gl-emoji>
+ `;
+}
+
+function installGlEmojiElement() {
+ const GlEmojiElementProto = Object.create(HTMLElement.prototype);
+ GlEmojiElementProto.createdCallback = function createdCallback() {
+ const emojiUnicode = this.textContent.trim();
+ const {
+ name,
+ unicodeVersion,
+ fallbackSrc,
+ fallbackSpriteClass,
+ } = this.dataset;
+
+ const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
+ this.childNodes,
+ childNode => childNode.nodeType === 3,
+ );
+ const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
+ const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
+
+ if (
+ isEmojiUnicode &&
+ !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
+ ) {
+ // CSS sprite fallback takes precedence over image fallback
+ if (hasCssSpriteFalback) {
+ // IE 11 doesn't like adding multiple at once :(
+ this.classList.add('emoji-icon');
+ this.classList.add(fallbackSpriteClass);
+ } else if (hasImageFallback) {
+ this.innerHTML = emojiImageTag(name, fallbackSrc);
+ } else {
+ const src = assembleFallbackImageSrc(name);
+ this.innerHTML = emojiImageTag(name, src);
+ }
+ }
+ };
+
+ document.registerElement('gl-emoji', {
+ prototype: GlEmojiElementProto,
+ });
+}
+
+export {
+ installGlEmojiElement,
+ glEmojiTag,
+ emojiImageTag,
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
new file mode 100644
index 00000000000..be4aeb32c46
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
@@ -0,0 +1,11 @@
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+
+function isEmojiNameValid(inputName) {
+ const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+
+ return name && emojiMap[name];
+}
+
+export default isEmojiNameValid;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
new file mode 100644
index 00000000000..5e3c45f7e92
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -0,0 +1,121 @@
+import spreadString from './spread_string';
+
+// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
+const flagACodePoint = 127462; // parseInt('1F1E6', 16)
+const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
+function isFlagEmoji(emojiUnicode) {
+ const cp = emojiUnicode.codePointAt(0);
+ // Length 4 because flags are made of 2 characters which are surrogate pairs
+ return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
+}
+
+// Chrome <57 renders keycaps oddly
+// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
+// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
+function isKeycapEmoji(emojiUnicode) {
+ return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
+}
+
+// Check for a skin tone variation emoji which aren't always supported
+const tone1 = 127995;// parseInt('1F3FB', 16)
+const tone5 = 127999;// parseInt('1F3FF', 16)
+function isSkinToneComboEmoji(emojiUnicode) {
+ return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+ const cp = char.codePointAt(0);
+ return cp >= tone1 && cp <= tone5;
+ });
+}
+
+// macOS supports most skin tone emoji's but
+// doesn't support the skin tone versions of horse racing
+const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
+function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
+ return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
+ isSkinToneComboEmoji(emojiUnicode);
+}
+
+// Check for `family_*`, `kiss_*`, `couple_*`
+// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
+const zwj = 8205; // parseInt('200D', 16)
+const personStartCodePoint = 128102; // parseInt('1F466', 16)
+const personEndCodePoint = 128105; // parseInt('1F469', 16)
+function isPersonZwjEmoji(emojiUnicode) {
+ let hasPersonEmoji = false;
+ let hasZwj = false;
+ spreadString(emojiUnicode).forEach((character) => {
+ const cp = character.codePointAt(0);
+ if (cp === zwj) {
+ hasZwj = true;
+ } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
+ hasPersonEmoji = true;
+ }
+ });
+
+ return hasPersonEmoji && hasZwj;
+}
+
+// Helper so we don't have to run `isFlagEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isFlagResult = isFlagEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.flag && isFlagResult) ||
+ !isFlagResult
+ );
+}
+
+// Helper so we don't have to run `isSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
+ const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
+ !isSkinToneResult
+ );
+}
+
+// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
+ !isHorseRacingSkinToneResult
+ );
+}
+
+// Helper so we don't have to run `isPersonZwjEmoji` twice
+// in `isEmojiUnicodeSupported` logic
+function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
+ const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
+ return (
+ (unicodeSupportMap.personZwj && isPersonZwjResult) ||
+ !isPersonZwjResult
+ );
+}
+
+// Takes in a support map and determines whether
+// the given unicode emoji is supported on the platform.
+//
+// Combines all the edge case tests into a one-stop shop method
+function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
+ const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
+ unicodeSupportMap.meta.chromeVersion < 57;
+
+ // For comments about each scenario, see the comments above each individual respective function
+ return unicodeSupportMap[unicodeVersion] &&
+ !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
+ checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+ checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
+ checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
+ checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
+}
+
+export {
+ isEmojiUnicodeSupported,
+ isFlagEmoji,
+ isKeycapEmoji,
+ isSkinToneComboEmoji,
+ isHorceRacingSkinToneComboEmoji,
+ isPersonZwjEmoji,
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
new file mode 100644
index 00000000000..327764ec6e9
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
@@ -0,0 +1,50 @@
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
+function knownCharCodeAt(givenString, index) {
+ const str = `${givenString}`;
+ const end = str.length;
+
+ const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
+ let idx = index;
+ while ((surrogatePairs.exec(str)) != null) {
+ const li = surrogatePairs.lastIndex;
+ if (li - 2 < idx) {
+ idx += 1;
+ } else {
+ break;
+ }
+ }
+
+ if (idx >= end || idx < 0) {
+ return NaN;
+ }
+
+ const code = str.charCodeAt(idx);
+
+ let high;
+ let low;
+ if (code >= 0xD800 && code <= 0xDBFF) {
+ high = code;
+ low = str.charCodeAt(idx + 1);
+ // Go one further, since one of the "characters" is part of a surrogate pair
+ return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
+ }
+ return code;
+}
+
+// See http://stackoverflow.com/a/38901550/796832
+// ES5/PhantomJS compatible version of spreading a string
+//
+// [...'foo'] -> ['f', 'o', 'o']
+// [...'🖐🏿'] -> ['🖐', '🏿']
+function spreadString(str) {
+ const arr = [];
+ let i = 0;
+ while (!isNaN(knownCharCodeAt(str, i))) {
+ const codePoint = knownCharCodeAt(str, i);
+ arr.push(String.fromCodePoint(codePoint));
+ i += 1;
+ }
+ return arr;
+}
+
+export default spreadString;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
new file mode 100644
index 00000000000..aa522e20c36
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js
@@ -0,0 +1,161 @@
+const unicodeSupportTestMap = {
+ // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
+ // occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
+ // woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
+ // sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
+ // family_mwgb
+ // Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
+ personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
+ // horse_racing_tone5
+ // Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
+ horseRacing: '\u{1F3C7}\u{1F3FF}',
+ // US flag, http://emojipedia.org/flags/
+ flag: '\u{1F1FA}\u{1F1F8}',
+ // http://emojipedia.org/modifiers/
+ skinToneModifier: [
+ // spy_tone5
+ '\u{1F575}\u{1F3FF}',
+ // person_with_ball_tone5
+ '\u{26F9}\u{1F3FF}',
+ // angel_tone5
+ '\u{1F47C}\u{1F3FF}',
+ ],
+ // rofl, http://emojipedia.org/unicode-9.0/
+ '9.0': '\u{1F923}',
+ // metal, http://emojipedia.org/unicode-8.0/
+ '8.0': '\u{1F918}',
+ // spy, http://emojipedia.org/unicode-7.0/
+ '7.0': '\u{1F575}',
+ // expressionless, http://emojipedia.org/unicode-6.1/
+ 6.1: '\u{1F611}',
+ // japanese_goblin, http://emojipedia.org/unicode-6.0/
+ '6.0': '\u{1F47A}',
+ // sailboat, http://emojipedia.org/unicode-5.2/
+ 5.2: '\u{26F5}',
+ // mahjong, http://emojipedia.org/unicode-5.1/
+ 5.1: '\u{1F004}',
+ // gear, http://emojipedia.org/unicode-4.1/
+ 4.1: '\u{2699}',
+ // zap, http://emojipedia.org/unicode-4.0/
+ '4.0': '\u{26A1}',
+ // recycle, http://emojipedia.org/unicode-3.2/
+ 3.2: '\u{267B}',
+ // information_source, http://emojipedia.org/unicode-3.0/
+ '3.0': '\u{2139}',
+ // heart, http://emojipedia.org/unicode-1.1/
+ 1.1: '\u{2764}',
+};
+
+function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
+ // `4 *` because RGBA
+ const indexOffset = 4 * pixelOffset;
+ const hasColor = imageDataArray[indexOffset + 0] ||
+ imageDataArray[indexOffset + 1] ||
+ imageDataArray[indexOffset + 2];
+ const isVisible = imageDataArray[indexOffset + 3];
+ // Check for some sort of color other than black
+ if (hasColor && isVisible) {
+ return true;
+ }
+ return false;
+}
+
+const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
+const isChrome = chromeMatches && chromeMatches.length > 0;
+const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
+
+// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
+// See 32px, https://i.imgur.com/htY6Zym.png
+// See 16px, https://i.imgur.com/FPPsIF8.png
+const fontSize = 16;
+function generateUnicodeSupportMap(testMap) {
+ const testMapKeys = Object.keys(testMap);
+ const numTestEntries = testMapKeys
+ .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+
+ const canvas = document.createElement('canvas');
+ (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
+ const ctx = canvas.getContext('2d');
+ canvas.width = (2 * fontSize);
+ canvas.height = (numTestEntries * fontSize);
+ ctx.fillStyle = '#000000';
+ ctx.textBaseline = 'middle';
+ ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
+ // Write each emoji to the canvas vertically
+ let writeIndex = 0;
+ testMapKeys.forEach((testKey) => {
+ const testEntry = testMap[testKey];
+ [].concat(testEntry).forEach((emojiUnicode) => {
+ ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+ writeIndex += 1;
+ });
+ });
+
+ // Read from the canvas
+ const resultMap = {};
+ let readIndex = 0;
+ testMapKeys.forEach((testKey) => {
+ const testEntry = testMap[testKey];
+ // This needs to be a `reduce` instead of `every` because we need to
+ // keep the `readIndex` in sync from the writes by running all entries
+ const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+ // Sample along the vertical-middle for a couple of characters
+ const imageData = ctx.getImageData(
+ 0,
+ (readIndex * fontSize) + (fontSize / 2),
+ 2 * fontSize,
+ 1,
+ ).data;
+
+ let isValidEmoji = false;
+ for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
+ const isLookingAtFirstChar = currentPixel < fontSize;
+ const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+ // Check for the emoji somewhere along the row
+ if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
+ isValidEmoji = true;
+
+ // Check to see that nothing is rendered next to the first character
+ // to ensure that the ZWJ sequence rendered as one piece
+ } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
+ isValidEmoji = false;
+ break;
+ }
+ }
+
+ readIndex += 1;
+ return isSatisfied && isValidEmoji;
+ }, true);
+
+ resultMap[testKey] = isTestSatisfied;
+ });
+
+ resultMap.meta = {
+ isChrome,
+ chromeVersion,
+ };
+
+ return resultMap;
+}
+
+function getUnicodeSupportMap() {
+ let unicodeSupportMap;
+ const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ try {
+ unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
+ } catch (err) {
+ // swallow
+ }
+ if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+ unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
+ window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
+ window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ }
+
+ return unicodeSupportMap;
+}
+
+export {
+ getUnicodeSupportMap,
+ generateUnicodeSupportMap,
+};
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index a7e68ae5cb9..626f3503c91 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -6,7 +6,7 @@
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
-require('../extensions/jquery');
+import '../commons/bootstrap';
//
// ### Example Markup
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 6b21695d082..eb7143f5b1a 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -4,7 +4,7 @@
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
-require('../extensions/jquery');
+import '../commons/bootstrap';
//
// ### Example Markup
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index a7181904ac9..92f3bb3ff52 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -22,8 +22,12 @@
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
- e.preventDefault();
toggleContainer($(this).closest('.js-toggle-container'));
+
+ const targetTag = e.target.tagName.toLowerCase();
+ if (targetTag === 'a' || targetTag === 'button') {
+ e.preventDefault();
+ }
});
// If we're accessing a permalink, ensure it is not inside a
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js
index ec1c018424d..ec1c018424d 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js
index d4f60cc6ecd..d4f60cc6ecd 100644
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
index 9cee79fa5d5..9cee79fa5d5 100644
--- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 5f14ff40eee..8f6bf162d6e 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -36,7 +36,7 @@
this.removeFile(file);
});
return this.on('sending', function(file, xhr, formData) {
- formData.append('target_branch', form.find('.js-target-branch').val());
+ formData.append('target_branch', form.find('input[name="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());
});
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js
index c5067b0feae..c5067b0feae 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
new file mode 100644
index 00000000000..c8f68860fbd
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -0,0 +1,35 @@
+const lineNumberRe = /^L[0-9]+/;
+
+const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
+ const hash = gl.utils.getLocationHash();
+ if (hash && lineNumberRe.test(hash)) {
+ const hashUrlString = `#${hash}`;
+
+ [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
+ const baseHref = permalinkButton.getAttribute('data-original-href') || (() => {
+ const href = permalinkButton.getAttribute('href');
+ permalinkButton.setAttribute('data-original-href', href);
+ return href;
+ })();
+ permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
+ });
+ }
+};
+
+function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
+ const updateBlameAndBlobPermalinkCb = () => {
+ // Wait for the hash to update from the LineHighlighter callback
+ setTimeout(() => {
+ updateLineNumbersOnBlobPermalinks(elementsToUpdate);
+ }, 0);
+ };
+
+ blobContentHolder.addEventListener('click', (e) => {
+ if (e.target.matches(lineNumberSelector)) {
+ updateBlameAndBlobPermalinkCb();
+ }
+ });
+ updateBlameAndBlobPermalinkCb();
+}
+
+export default BlobLinePermalinkUpdater;
diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js
new file mode 100644
index 00000000000..95517f51b1c
--- /dev/null
+++ b/app/assets/javascripts/blob/create_branch_dropdown.js
@@ -0,0 +1,88 @@
+class CreateBranchDropdown {
+ constructor(el, targetBranchDropdown) {
+ this.targetBranchDropdown = targetBranchDropdown;
+ this.el = el;
+ this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
+ this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
+ this.newBranchField = this.el.querySelector('#new_branch_name');
+ this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
+
+ this.newBranchCreateButton.setAttribute('disabled', '');
+
+ this.addBindings();
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ this.cleanBindings();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanBindings() {
+ this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
+ this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
+ }
+
+ addBindings() {
+ this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
+ this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
+ this.resetFormWrapper = this.resetForm.bind(this);
+ this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
+ this.createBranchWrapper = this.createBranch.bind(this);
+
+ this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
+ this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
+ this.dropdownBack.addEventListener('click', this.resetFormWrapper);
+ this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
+ this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
+ }
+
+ handleCancelClick(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.resetForm();
+ this.dropdownBack.click();
+ }
+
+ handleNewBranchKeydown(e) {
+ const keyCode = e.which;
+ const ENTER_KEYCODE = 13;
+ if (keyCode === ENTER_KEYCODE) {
+ this.createBranch(e);
+ }
+ }
+
+ enableBranchCreateButton() {
+ if (this.newBranchField.value !== '') {
+ this.newBranchCreateButton.removeAttribute('disabled');
+ } else {
+ this.newBranchCreateButton.setAttribute('disabled', '');
+ }
+ }
+
+ resetForm() {
+ this.newBranchField.value = '';
+ this.enableBranchCreateButtonWrapper();
+ }
+
+ createBranch(e) {
+ e.preventDefault();
+
+ if (this.newBranchCreateButton.getAttribute('disabled') === '') {
+ return;
+ }
+ const newBranchName = this.newBranchField.value;
+ this.targetBranchDropdown.setNewBranch(newBranchName);
+ this.resetForm();
+ }
+}
+
+window.gl = window.gl || {};
+gl.CreateBranchDropdown = CreateBranchDropdown;
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
new file mode 100644
index 00000000000..216f069ef71
--- /dev/null
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -0,0 +1,152 @@
+/* eslint-disable class-methods-use-this */
+const SELECT_ITEM_MSG = 'Select';
+
+class TargetBranchDropDown {
+ constructor(dropdown) {
+ this.dropdown = dropdown;
+ this.$dropdown = $(dropdown);
+ this.fieldName = this.dropdown.getAttribute('data-field-name');
+ this.form = this.dropdown.closest('form');
+ this.createDropdown();
+ }
+
+ static bootstrap() {
+ const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
+ [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
+ }
+
+ createDropdown() {
+ const self = this;
+ this.$dropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ search: {
+ fields: ['title'],
+ },
+ data: (term, callback) => $.ajax({
+ url: self.dropdown.getAttribute('data-refs-url'),
+ data: {
+ ref: self.dropdown.getAttribute('data-ref'),
+ show_all: true,
+ },
+ dataType: 'json',
+ }).done(refs => callback(self.dropdownData(refs))),
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ }
+ return SELECT_ITEM_MSG;
+ },
+ clicked(item, el, e) {
+ e.preventDefault();
+ self.onClick.call(self);
+ },
+ fieldName: self.fieldName,
+ });
+ return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
+ }
+
+ onClick() {
+ this.enableSubmit();
+ this.$dropdown.trigger('change.branch');
+ }
+
+ enableSubmit() {
+ const submitBtn = this.form.querySelector('[type="submit"]');
+ if (this.branchInput && this.branchInput.value) {
+ submitBtn.removeAttribute('disabled');
+ } else {
+ submitBtn.setAttribute('disabled', '');
+ }
+ }
+
+ dropdownData(refs) {
+ const branchList = this.dropdownItems(refs);
+ this.cachedRefs = refs;
+ this.addDefaultBranch(branchList);
+ this.addNewBranch(branchList);
+ return { Branches: branchList };
+ }
+
+ dropdownItems(refs) {
+ return refs.map(this.dropdownItem);
+ }
+
+ dropdownItem(ref) {
+ return { id: ref, text: ref, title: ref };
+ }
+
+ addDefaultBranch(branchList) {
+ // when no branch is selected do nothing
+ if (!this.branchInput) {
+ return;
+ }
+
+ const branchInputVal = this.branchInput.value;
+ const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
+
+ if (currentBranchIndex === -1) {
+ this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
+ }
+ }
+
+ addNewBranch(branchList) {
+ if (this.newBranch) {
+ this.unshiftBranch(branchList, this.newBranch);
+ }
+ }
+
+ searchBranch(branchList, branchName) {
+ return _.findIndex(branchList, el => branchName === el.id);
+ }
+
+ unshiftBranch(branchList, branch) {
+ const branchIndex = this.searchBranch(branchList, branch.id);
+
+ if (branchIndex === -1) {
+ branchList.unshift(branch);
+ }
+ }
+
+ setNewBranch(newBranchName) {
+ this.newBranch = this.dropdownItem(newBranchName);
+ this.refreshData();
+ this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
+ }
+
+ refreshData() {
+ this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
+ this.clearFilter();
+ }
+
+ clearFilter() {
+ // apply an empty filter in order to refresh the data
+ this.glDropdown.filter.filter('');
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
+ }
+
+ selectBranch(index) {
+ const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
+
+ if (!branch.classList.contains('is-active')) {
+ branch.click();
+ } else {
+ this.closeDropdown();
+ }
+ }
+
+ closeDropdown() {
+ this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
+ }
+
+ get branchInput() {
+ return this.form.querySelector(`input[name="${this.fieldName}"]`);
+ }
+
+ get glDropdown() {
+ return this.$dropdown.data('glDropdown');
+ }
+}
+
+window.gl = window.gl || {};
+gl.TargetBranchDropDown = TargetBranchDropDown;
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js
index 7e03ec3b391..7e03ec3b391 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
new file mode 100644
index 00000000000..3874c2819a5
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -0,0 +1,167 @@
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
+import FilteredSearchBoards from './filtered_search_boards';
+import eventHub from './eventhub';
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('./models/issue');
+require('./models/label');
+require('./models/list');
+require('./models/milestone');
+require('./models/user');
+require('./stores/boards_store');
+require('./stores/modal_store');
+require('./services/board_service');
+require('./mixins/modal_mixins');
+require('./mixins/sortable_default_options');
+require('./filters/due_date_filters');
+require('./components/board');
+require('./components/board_sidebar');
+require('./components/new_list_dropdown');
+require('./components/modal/index');
+require('../vue_shared/vue_resource_interceptor');
+
+$(() => {
+ const $boardApp = document.getElementById('board-app');
+ const Store = gl.issueBoards.BoardsStore;
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ window.gl = window.gl || {};
+
+ if (gl.IssueBoardsApp) {
+ gl.IssueBoardsApp.$destroy(true);
+ }
+
+ Store.create();
+
+ gl.IssueBoardsApp = new Vue({
+ el: $boardApp,
+ components: {
+ 'board': gl.issueBoards.Board,
+ 'board-sidebar': gl.issueBoards.BoardSidebar,
+ 'board-add-issues-modal': gl.issueBoards.IssuesModal,
+ },
+ data: {
+ state: Store.state,
+ loading: true,
+ endpoint: $boardApp.dataset.endpoint,
+ boardId: $boardApp.dataset.boardId,
+ disabled: $boardApp.dataset.disabled === 'true',
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
+ detailIssue: Store.detail
+ },
+ computed: {
+ detailIssueVisible () {
+ return Object.keys(this.detailIssue.issue).length;
+ },
+ },
+ created () {
+ gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
+
+ this.filterManager = new FilteredSearchBoards(Store.filter, true);
+
+ // Listen for updateTokens event
+ eventHub.$on('updateTokens', this.updateTokens);
+ },
+ beforeDestroy() {
+ eventHub.$off('updateTokens', this.updateTokens);
+ },
+ mounted () {
+ Store.disabled = this.disabled;
+ gl.boardService.all()
+ .then((resp) => {
+ resp.json().forEach((board) => {
+ const list = Store.addList(board);
+
+ if (list.type === 'done') {
+ list.position = Infinity;
+ }
+ });
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+
+ Store.addBlankState();
+ this.loading = false;
+ });
+ },
+ methods: {
+ updateTokens() {
+ this.filterManager.updateTokens();
+ }
+ },
+ });
+
+ gl.IssueBoardsSearch = new Vue({
+ el: document.getElementById('js-add-list'),
+ data: {
+ filters: Store.state.filters
+ },
+ mounted () {
+ gl.issueBoards.newListDropdownInit();
+ }
+ });
+
+ gl.IssueBoardsModalAddBtn = new Vue({
+ mixins: [gl.issueBoards.ModalMixins],
+ el: document.getElementById('js-add-issues-btn'),
+ data: {
+ modal: ModalStore.store,
+ store: Store.state,
+ },
+ watch: {
+ disabled() {
+ this.updateTooltip();
+ },
+ },
+ computed: {
+ disabled() {
+ return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
+ },
+ tooltipTitle() {
+ if (this.disabled) {
+ return 'Please add a list to your board first';
+ }
+
+ return '';
+ },
+ },
+ methods: {
+ updateTooltip() {
+ const $tooltip = $(this.$el);
+
+ this.$nextTick(() => {
+ if (this.disabled) {
+ $tooltip.tooltip();
+ } else {
+ $tooltip.tooltip('destroy');
+ }
+ });
+ },
+ openModal() {
+ if (!this.disabled) {
+ this.toggleModal(true);
+ }
+ },
+ },
+ mounted() {
+ this.updateTooltip();
+ },
+ template: `
+ <button
+ class="btn btn-create pull-right prepend-left-10"
+ type="button"
+ data-placement="bottom"
+ :class="{ 'disabled': disabled }"
+ :title="tooltipTitle"
+ :aria-disabled="disabled"
+ @click="openModal">
+ Add issues
+ </button>
+ `,
+ });
+});
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
deleted file mode 100644
index 878ad1b6031..00000000000
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ /dev/null
@@ -1,111 +0,0 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
-/* global Vue */
-/* global BoardService */
-
-function requireAll(context) { return context.keys().map(context); }
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
-require('./components/board');
-require('./components/board_sidebar');
-require('./components/new_list_dropdown');
-require('./components/modal/index');
-require('../vue_shared/vue_resource_interceptor');
-
-$(() => {
- const $boardApp = document.getElementById('board-app');
- const Store = gl.issueBoards.BoardsStore;
- const ModalStore = gl.issueBoards.ModalStore;
-
- window.gl = window.gl || {};
-
- if (gl.IssueBoardsApp) {
- gl.IssueBoardsApp.$destroy(true);
- }
-
- Store.create();
-
- gl.IssueBoardsApp = new Vue({
- el: $boardApp,
- components: {
- 'board': gl.issueBoards.Board,
- 'board-sidebar': gl.issueBoards.BoardSidebar,
- 'board-add-issues-modal': gl.issueBoards.IssuesModal,
- },
- data: {
- state: Store.state,
- loading: true,
- endpoint: $boardApp.dataset.endpoint,
- boardId: $boardApp.dataset.boardId,
- disabled: $boardApp.dataset.disabled === 'true',
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
- bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
- detailIssue: Store.detail
- },
- computed: {
- detailIssueVisible () {
- return Object.keys(this.detailIssue.issue).length;
- },
- },
- created () {
- gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
- },
- mounted () {
- Store.disabled = this.disabled;
- gl.boardService.all()
- .then((resp) => {
- resp.json().forEach((board) => {
- const list = Store.addList(board);
-
- if (list.type === 'done') {
- list.position = Infinity;
- }
- });
-
- this.state.lists = _.sortBy(this.state.lists, 'position');
-
- Store.addBlankState();
- this.loading = false;
- });
- }
- });
-
- gl.IssueBoardsSearch = new Vue({
- el: document.getElementById('js-boards-search'),
- data: {
- filters: Store.state.filters
- },
- mounted () {
- gl.issueBoards.newListDropdownInit();
- }
- });
-
- gl.IssueBoardsModalAddBtn = new Vue({
- mixins: [gl.issueBoards.ModalMixins],
- el: document.getElementById('js-add-issues-btn'),
- data: {
- modal: ModalStore.store,
- store: Store.state,
- },
- computed: {
- disabled() {
- return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
- },
- },
- template: `
- <button
- class="btn btn-create pull-right prepend-left-10 has-tooltip"
- type="button"
- :disabled="disabled"
- @click="toggleModal(true)">
- Add issues
- </button>
- `,
- });
-});
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
new file mode 100644
index 00000000000..67c0c419713
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js
@@ -0,0 +1,106 @@
+/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* global Vue */
+/* global Sortable */
+
+import boardBlankState from './board_blank_state';
+
+require('./board_delete');
+require('./board_list');
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.Board = Vue.extend({
+ template: '#js-board-template',
+ components: {
+ 'board-list': gl.issueBoards.BoardList,
+ 'board-delete': gl.issueBoards.BoardDelete,
+ boardBlankState,
+ },
+ props: {
+ list: Object,
+ disabled: Boolean,
+ issueLinkBase: String,
+ rootPath: String,
+ },
+ data () {
+ return {
+ detailIssue: Store.detail,
+ filter: Store.filter,
+ };
+ },
+ watch: {
+ filter: {
+ handler() {
+ this.list.page = 1;
+ this.list.getIssues(true);
+ },
+ deep: true,
+ },
+ detailIssue: {
+ handler () {
+ if (!Object.keys(this.detailIssue.issue).length) return;
+
+ const issue = this.list.findIssue(this.detailIssue.issue.id);
+
+ if (issue) {
+ const offsetLeft = this.$el.offsetLeft;
+ const boardsList = document.querySelectorAll('.boards-list')[0];
+ const left = boardsList.scrollLeft - offsetLeft;
+ let right = (offsetLeft + this.$el.offsetWidth);
+
+ if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
+ // -290 here because width of boardsList is animating so therefore
+ // getting the width here is incorrect
+ // 290 is the width of the sidebar
+ right -= (boardsList.offsetWidth - 290);
+ } else {
+ right -= boardsList.offsetWidth;
+ }
+
+ if (right - boardsList.scrollLeft > 0) {
+ $(boardsList).animate({
+ scrollLeft: right
+ }, this.sortableOptions.animation);
+ } else if (left > 0) {
+ $(boardsList).animate({
+ scrollLeft: offsetLeft
+ }, this.sortableOptions.animation);
+ }
+ }
+ },
+ deep: true
+ }
+ },
+ methods: {
+ showNewIssueForm() {
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+ }
+ },
+ mounted () {
+ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd: (e) => {
+ gl.issueBoards.onEnd();
+
+ if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+ const order = this.sortable.toArray();
+ const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
+
+ this.$nextTick(() => {
+ Store.moveList(list, order);
+ });
+ }
+ }
+ });
+
+ this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
+ },
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
deleted file mode 100644
index 18324de18b3..00000000000
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ /dev/null
@@ -1,105 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var */
-/* global Vue */
-/* global Sortable */
-
-require('./board_blank_state');
-require('./board_delete');
-require('./board_list');
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.Board = Vue.extend({
- template: '#js-board-template',
- components: {
- 'board-list': gl.issueBoards.BoardList,
- 'board-delete': gl.issueBoards.BoardDelete,
- 'board-blank-state': gl.issueBoards.BoardBlankState
- },
- props: {
- list: Object,
- disabled: Boolean,
- issueLinkBase: String,
- rootPath: String,
- },
- data () {
- return {
- detailIssue: Store.detail,
- filters: Store.state.filters,
- };
- },
- watch: {
- filters: {
- handler () {
- this.list.page = 1;
- this.list.getIssues(true);
- },
- deep: true
- },
- detailIssue: {
- handler () {
- if (!Object.keys(this.detailIssue.issue).length) return;
-
- const issue = this.list.findIssue(this.detailIssue.issue.id);
-
- if (issue) {
- const offsetLeft = this.$el.offsetLeft;
- const boardsList = document.querySelectorAll('.boards-list')[0];
- const left = boardsList.scrollLeft - offsetLeft;
- let right = (offsetLeft + this.$el.offsetWidth);
-
- if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
- // -290 here because width of boardsList is animating so therefore
- // getting the width here is incorrect
- // 290 is the width of the sidebar
- right -= (boardsList.offsetWidth - 290);
- } else {
- right -= boardsList.offsetWidth;
- }
-
- if (right - boardsList.scrollLeft > 0) {
- $(boardsList).animate({
- scrollLeft: right
- }, this.sortableOptions.animation);
- } else if (left > 0) {
- $(boardsList).animate({
- scrollLeft: offsetLeft
- }, this.sortableOptions.animation);
- }
- }
- },
- deep: true
- }
- },
- methods: {
- showNewIssueForm() {
- this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
- }
- },
- mounted () {
- this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
- disabled: this.disabled,
- group: 'boards',
- draggable: '.is-draggable',
- handle: '.js-board-handle',
- onEnd: (e) => {
- gl.issueBoards.onEnd();
-
- if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray();
- const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
-
- this.$nextTick(() => {
- Store.moveList(list, order);
- });
- }
- }
- });
-
- this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
- },
- });
-})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
new file mode 100644
index 00000000000..52893d4642b
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -0,0 +1,84 @@
+/* global ListLabel */
+/* global Cookies */
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+ template: `
+ <div class="board-blank-state">
+ <p>
+ Add the following default lists to your Issue Board with one click:
+ </p>
+ <ul class="board-blank-state-list">
+ <li v-for="label in predefinedLabels">
+ <span
+ class="label-color"
+ :style="{ backgroundColor: label.color }">
+ </span>
+ {{ label.title }}
+ </li>
+ </ul>
+ <p>
+ Starting out with the default set of lists will get you right on the way to making the most of your board.
+ </p>
+ <button
+ class="btn btn-create btn-inverted btn-block"
+ type="button"
+ @click.stop="addDefaultLists">
+ Add default lists
+ </button>
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click.stop="clearBlankState">
+ Nevermind, I'll use my own
+ </button>
+ </div>
+ `,
+ data() {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Doing', color: '#5CB85C' }),
+ ],
+ };
+ },
+ methods: {
+ addDefaultLists() {
+ this.clearBlankState();
+
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
+ title: label.title,
+ color: label.color,
+ },
+ });
+ });
+
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
+
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
+ });
+ })
+ .catch(() => {
+ Store.removeList(undefined, 'label');
+ Cookies.remove('issue_board_welcome_hidden', {
+ path: '',
+ });
+ Store.addBlankState();
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store),
+ },
+};
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
deleted file mode 100644
index d76314c1892..00000000000
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable space-before-function-paren, comma-dangle */
-/* global Vue */
-/* global ListLabel */
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardBlankState = Vue.extend({
- data () {
- return {
- predefinedLabels: [
- new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
- new ListLabel({ title: 'Doing', color: '#5CB85C' })
- ]
- };
- },
- methods: {
- addDefaultLists () {
- this.clearBlankState();
-
- this.predefinedLabels.forEach((label, i) => {
- Store.addList({
- title: label.title,
- position: i,
- list_type: 'label',
- label: {
- title: label.title,
- color: label.color
- }
- });
- });
-
- Store.state.lists = _.sortBy(Store.state.lists, 'position');
-
- // Save the labels
- gl.boardService.generateDefaultLists()
- .then((resp) => {
- resp.json().forEach((listObj) => {
- const list = Store.findList('title', listObj.title);
-
- list.id = listObj.id;
- list.label.id = listObj.label.id;
- list.getIssues();
- });
- });
- },
- clearBlankState: Store.removeBlankState.bind(Store)
- }
- });
-})();
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
new file mode 100644
index 00000000000..4b72090df31
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -0,0 +1,70 @@
+/* global Vue */
+require('./issue_card_inner');
+
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+ name: 'BoardsIssueCard',
+ template: `
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+ `,
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number,
+ rootPath: String,
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ const targetTagName = e.target.tagName.toLowerCase();
+
+ if (targetTagName === 'a' || targetTagName === 'button') return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ Store.detail.issue = {};
+ } else {
+ Store.detail.issue = this.issue;
+ Store.detail.list = this.list;
+ }
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
deleted file mode 100644
index 0ea66bd027c..00000000000
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
-/* global Vue */
-
-require('./issue_card_inner');
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardCard = Vue.extend({
- template: '#js-board-list-card',
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- props: {
- list: Object,
- issue: Object,
- issueLinkBase: String,
- disabled: Boolean,
- index: Number,
- rootPath: String,
- },
- data () {
- return {
- showDetail: false,
- detailIssue: Store.detail
- };
- },
- computed: {
- issueDetailVisible () {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- }
- },
- methods: {
- mouseDown () {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue (e) {
- const targetTagName = e.target.tagName.toLowerCase();
-
- if (targetTagName === 'a' || targetTagName === 'button') return;
-
- if (this.showDetail) {
- this.showDetail = false;
-
- if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
- } else {
- Store.detail.issue = this.issue;
- Store.detail.list = this.list;
- }
- }
- }
- }
- });
-})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js
index 861600424a5..861600424a5 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
new file mode 100644
index 00000000000..1330d4ae840
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -0,0 +1,131 @@
+/* eslint-disable comma-dangle, space-before-function-paren, max-len */
+/* global Vue */
+/* global Sortable */
+
+import boardNewIssue from './board_new_issue';
+import boardCard from './board_card';
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardList = Vue.extend({
+ template: '#js-board-list-template',
+ components: {
+ boardCard,
+ boardNewIssue,
+ },
+ props: {
+ disabled: Boolean,
+ list: Object,
+ issues: Array,
+ loading: Boolean,
+ issueLinkBase: String,
+ rootPath: String,
+ },
+ data () {
+ return {
+ scrollOffset: 250,
+ filters: Store.state.filters,
+ showCount: false,
+ showIssueForm: false
+ };
+ },
+ watch: {
+ filters: {
+ handler () {
+ this.list.loadingMore = false;
+ this.$refs.list.scrollTop = 0;
+ },
+ deep: true
+ },
+ issues () {
+ this.$nextTick(() => {
+ if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
+ this.list.page += 1;
+ this.list.getIssues(false);
+ }
+
+ if (this.scrollHeight() > this.listHeight()) {
+ this.showCount = true;
+ } else {
+ this.showCount = false;
+ }
+ });
+ }
+ },
+ methods: {
+ listHeight () {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight () {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop () {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ loadNextPage () {
+ const getIssues = this.list.nextPage();
+
+ if (getIssues) {
+ this.list.loadingMore = true;
+ getIssues.then(() => {
+ this.list.loadingMore = false;
+ });
+ }
+ },
+ toggleForm() {
+ this.showIssueForm = !this.showIssueForm;
+ },
+ },
+ created() {
+ gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
+ },
+ mounted () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ scroll: document.querySelectorAll('.boards-list')[0],
+ group: 'issues',
+ disabled: this.disabled,
+ filter: '.board-list-count, .is-disabled',
+ dataIdAttr: 'data-issue-id',
+ onStart: (e) => {
+ const card = this.$refs.issue[e.oldIndex];
+
+ card.showDetail = false;
+ Store.moving.list = card.list;
+ Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
+
+ gl.issueBoards.onStart();
+ },
+ onAdd: (e) => {
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
+
+ this.$nextTick(() => {
+ e.item.remove();
+ });
+ },
+ onUpdate: (e) => {
+ const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
+ gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
+ },
+ onMove(e) {
+ return !e.related.classList.contains('board-list-count');
+ }
+ });
+
+ this.sortable = Sortable.create(this.$refs.list, options);
+
+ // Scroll event on list to load more
+ this.$refs.list.onscroll = () => {
+ if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ this.loadNextPage();
+ }
+ };
+ },
+ beforeDestroy() {
+ gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
+ },
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
deleted file mode 100644
index 60b0a30af3f..00000000000
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ /dev/null
@@ -1,120 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, max-len */
-/* global Vue */
-/* global Sortable */
-
-require('./board_card');
-require('./board_new_issue');
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardList = Vue.extend({
- template: '#js-board-list-template',
- components: {
- 'board-card': gl.issueBoards.BoardCard,
- 'board-new-issue': gl.issueBoards.BoardNewIssue
- },
- props: {
- disabled: Boolean,
- list: Object,
- issues: Array,
- loading: Boolean,
- issueLinkBase: String,
- rootPath: String,
- },
- data () {
- return {
- scrollOffset: 250,
- filters: Store.state.filters,
- showCount: false,
- showIssueForm: false
- };
- },
- watch: {
- filters: {
- handler () {
- this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
- },
- deep: true
- },
- issues () {
- this.$nextTick(() => {
- if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
- this.list.page += 1;
- this.list.getIssues(false);
- }
-
- if (this.scrollHeight() > this.listHeight()) {
- this.showCount = true;
- } else {
- this.showCount = false;
- }
- });
- }
- },
- computed: {
- orderedIssues () {
- return _.sortBy(this.issues, 'priority');
- },
- },
- methods: {
- listHeight () {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight () {
- return this.$refs.list.scrollHeight;
- },
- scrollTop () {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- loadNextPage () {
- const getIssues = this.list.nextPage();
-
- if (getIssues) {
- this.list.loadingMore = true;
- getIssues.then(() => {
- this.list.loadingMore = false;
- });
- }
- },
- },
- mounted () {
- const options = gl.issueBoards.getBoardSortableDefaultOptions({
- scroll: document.querySelectorAll('.boards-list')[0],
- group: 'issues',
- sort: false,
- disabled: this.disabled,
- filter: '.board-list-count, .is-disabled',
- onStart: (e) => {
- const card = this.$refs.issue[e.oldIndex];
-
- card.showDetail = false;
- Store.moving.list = card.list;
- Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
-
- gl.issueBoards.onStart();
- },
- onAdd: (e) => {
- gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
-
- this.$nextTick(() => {
- e.item.remove();
- });
- },
- });
-
- this.sortable = Sortable.create(this.$refs.list, options);
-
- // Scroll event on list to load more
- this.$refs.list.onscroll = () => {
- if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
- this.loadNextPage();
- }
- };
- }
- });
-})();
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
new file mode 100644
index 00000000000..b88f59dd6d4
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -0,0 +1,92 @@
+/* global ListIssue */
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+ name: 'BoardNewIssue',
+ props: {
+ list: Object,
+ },
+ data() {
+ return {
+ title: '',
+ error: false,
+ };
+ },
+ methods: {
+ submit(e) {
+ e.preventDefault();
+ if (this.title.trim() === '') return;
+
+ this.error = false;
+
+ const labels = this.list.label ? [this.list.label] : [];
+ const issue = new ListIssue({
+ title: this.title,
+ labels,
+ subscribed: true,
+ });
+
+ this.list.newIssue(issue)
+ .then(() => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$refs.submitButton).enable();
+
+ Store.detail.issue = issue;
+ Store.detail.list = this.list;
+ })
+ .catch(() => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$refs.submitButton).enable();
+
+ // Remove the issue
+ this.list.removeIssue(issue);
+
+ // Show error message
+ this.error = true;
+ });
+
+ this.cancel();
+ },
+ cancel() {
+ this.title = '';
+ gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
+ },
+ },
+ mounted() {
+ this.$refs.input.focus();
+ },
+ template: `
+ <div class="card board-new-issue-form">
+ <form @submit="submit($event)">
+ <div class="flash-container"
+ v-if="error">
+ <div class="flash-alert">
+ An error occured. Please try again.
+ </div>
+ </div>
+ <label class="label-light"
+ :for="list.id + '-title'">
+ Title
+ </label>
+ <input class="form-control"
+ type="text"
+ v-model="title"
+ ref="input"
+ :id="list.id + '-title'" />
+ <div class="clearfix prepend-top-10">
+ <button class="btn btn-success pull-left"
+ type="submit"
+ :disabled="title === ''"
+ ref="submit-button">
+ Submit issue
+ </button>
+ <button class="btn btn-default pull-right"
+ type="button"
+ @click="cancel">
+ Cancel
+ </button>
+ </div>
+ </form>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
deleted file mode 100644
index b5c14a198ba..00000000000
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable comma-dangle, no-unused-vars */
-/* global Vue */
-/* global ListIssue */
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
-
- gl.issueBoards.BoardNewIssue = Vue.extend({
- props: {
- list: Object,
- },
- data() {
- return {
- title: '',
- error: false
- };
- },
- methods: {
- submit(e) {
- e.preventDefault();
- if (this.title.trim() === '') return;
-
- this.error = false;
-
- const labels = this.list.label ? [this.list.label] : [];
- const issue = new ListIssue({
- title: this.title,
- labels,
- subscribed: true
- });
-
- this.list.newIssue(issue)
- .then((data) => {
- // Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$refs.submitButton).enable();
-
- Store.detail.issue = issue;
- Store.detail.list = this.list;
- })
- .catch(() => {
- // Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$refs.submitButton).enable();
-
- // Remove the issue
- this.list.removeIssue(issue);
-
- // Show error message
- this.error = true;
- });
-
- this.cancel();
- },
- cancel() {
- this.title = '';
- this.$parent.showIssueForm = false;
- }
- },
- mounted() {
- this.$refs.input.focus();
- },
- });
-})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js
index dfc6eed785c..dfc6eed785c 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
new file mode 100644
index 00000000000..69e30cec4c5
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -0,0 +1,114 @@
+/* global Vue */
+import eventHub from '../eventhub';
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.IssueCardInner = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ showLabel(label) {
+ if (!this.list) return true;
+
+ return !this.list.label || label.id !== this.list.label.id;
+ },
+ filterByLabel(label, e) {
+ if (!this.updateFilters) return;
+
+ const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ filterPath.push(param);
+ } else {
+ filterPath.splice(labelIndex, 1);
+ }
+
+ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
+
+ Store.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ template: `
+ <div>
+ <h4 class="card-title">
+ <i
+ class="fa fa-eye-slash confidential-icon"
+ v-if="issue.confidential"></i>
+ <a
+ :href="issueLinkBase + '/' + issue.id"
+ :title="issue.title">
+ {{ issue.title }}
+ </a>
+ </h4>
+ <div class="card-footer">
+ <span
+ class="card-number"
+ v-if="issue.id">
+ #{{ issue.id }}
+ </span>
+ <a
+ class="card-assignee has-tooltip"
+ :href="rootPath + issue.assignee.username"
+ :title="'Assigned to ' + issue.assignee.name"
+ v-if="issue.assignee"
+ data-container="body">
+ <img
+ class="avatar avatar-inline s20"
+ :src="issue.assignee.avatar"
+ width="20"
+ height="20"
+ :alt="'Avatar for ' + issue.assignee.name" />
+ </a>
+ <button
+ class="label color-label has-tooltip"
+ v-for="label in issue.labels"
+ type="button"
+ v-if="showLabel(label)"
+ @click="filterByLabel(label, $event)"
+ :style="labelStyle(label)"
+ :title="label.description"
+ data-container="body">
+ {{ label.title }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
deleted file mode 100644
index 22a8b971ff8..00000000000
--- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6
+++ /dev/null
@@ -1,111 +0,0 @@
-/* global Vue */
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.IssueCardInner = Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- },
- rootPath: {
- type: String,
- required: true,
- },
- },
- methods: {
- showLabel(label) {
- if (!this.list) return true;
-
- return !this.list.label || label.id !== this.list.label.id;
- },
- filterByLabel(label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters.label_name.indexOf(label.title);
- $(e.currentTarget).tooltip('hide');
-
- if (labelIndex === -1) {
- Store.state.filters.label_name.push(label.title);
- $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
- } else {
- Store.state.filters.label_name.splice(labelIndex, 1);
- labelToggleText = Store.state.filters.label_name[0];
- $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
- }
-
- const selectedLabels = Store.state.filters.label_name;
- if (selectedLabels.length === 0) {
- labelToggleText = 'Label';
- } else if (selectedLabels.length > 1) {
- labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
- }
-
- $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
-
- Store.updateFiltersUrl();
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- template: `
- <div>
- <h4 class="card-title">
- <i
- class="fa fa-eye-slash confidential-icon"
- v-if="issue.confidential"></i>
- <a
- :href="issueLinkBase + '/' + issue.id"
- :title="issue.title">
- {{ issue.title }}
- </a>
- </h4>
- <div class="card-footer">
- <span
- class="card-number"
- v-if="issue.id">
- #{{ issue.id }}
- </span>
- <a
- class="card-assignee has-tooltip"
- :href="rootPath + issue.assignee.username"
- :title="'Assigned to ' + issue.assignee.name"
- v-if="issue.assignee"
- data-container="body">
- <img
- class="avatar avatar-inline s20"
- :src="issue.assignee.avatar"
- width="20"
- height="20"
- :alt="'Avatar for ' + issue.assignee.name" />
- </a>
- <button
- class="label color-label has-tooltip"
- v-for="label in issue.labels"
- type="button"
- v-if="showLabel(label)"
- @click="filterByLabel(label, $event)"
- :style="labelStyle(label)"
- :title="label.description"
- data-container="body">
- {{ label.title }}
- </button>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
new file mode 100644
index 00000000000..e6973c3fd59
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -0,0 +1,70 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalEmptyState = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ props: {
+ image: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ An issue can be a bug, a todo or a feature request that needs to be
+ discussed in a project. Besides, issues are searchable and filterable.
+ `,
+ };
+
+ if (this.activeTab === 'selected') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>Open issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
+
+ return obj;
+ },
+ },
+ template: `
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <aside class="svg-content" v-html="image"></aside>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ v-if="activeTab === 'all'">
+ New issue
+ </a>
+ <button
+ type="button"
+ class="btn btn-default"
+ @click="changeTab('all')"
+ v-if="activeTab === 'selected'">
+ Open issues
+ </button>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
deleted file mode 100644
index 9538f5b69e9..00000000000
--- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6
+++ /dev/null
@@ -1,70 +0,0 @@
-/* global Vue */
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
-
- gl.issueBoards.ModalEmptyState = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- data() {
- return ModalStore.store;
- },
- props: {
- image: {
- type: String,
- required: true,
- },
- newIssuePath: {
- type: String,
- required: true,
- },
- },
- computed: {
- contents() {
- const obj = {
- title: 'You haven\'t added any issues to your project yet',
- content: `
- An issue can be a bug, a todo or a feature request that needs to be
- discussed in a project. Besides, issues are searchable and filterable.
- `,
- };
-
- if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
- obj.content = `
- Go back to <strong>All issues</strong> and select some issues
- to add to your board.
- `;
- }
-
- return obj;
- },
- },
- template: `
- <section class="empty-state">
- <div class="row">
- <div class="col-xs-12 col-sm-6 col-sm-push-6">
- <aside class="svg-content" v-html="image"></aside>
- </div>
- <div class="col-xs-12 col-sm-6 col-sm-pull-6">
- <div class="text-content">
- <h4>{{ contents.title }}</h4>
- <p v-html="contents.content"></p>
- <a
- :href="newIssuePath"
- class="btn btn-success btn-inverted"
- v-if="activeTab === 'all'">
- New issue
- </a>
- <button
- type="button"
- class="btn btn-default"
- @click="changeTab('all')"
- v-if="activeTab === 'selected'">
- All issues
- </button>
- </div>
- </div>
- </div>
- </section>
- `,
- });
-})();
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
new file mode 100644
index 00000000000..bd394a2318c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -0,0 +1,24 @@
+import FilteredSearchBoards from '../../filtered_search_boards';
+import FilteredSearchContainer from '../../../filtered_search/container';
+
+export default {
+ name: 'modal-filters',
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+ mounted() {
+ FilteredSearchContainer.container = this.$el;
+
+ this.filteredSearch = new FilteredSearchBoards(this.store);
+ this.filteredSearch.removeTokens();
+ },
+ beforeDestroy() {
+ this.filteredSearch.cleanup();
+ FilteredSearchContainer.container = document;
+ this.store.path = '';
+ },
+ template: '#js-board-modal-filter',
+};
diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6
deleted file mode 100644
index 6de06811d94..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters.js.es6
+++ /dev/null
@@ -1,49 +0,0 @@
-/* global Vue */
-const userFilter = require('./filters/user');
-const milestoneFilter = require('./filters/milestone');
-const labelFilter = require('./filters/label');
-
-module.exports = Vue.extend({
- name: 'modal-filters',
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- destroyed() {
- gl.issueBoards.ModalStore.setDefaultFilter();
- },
- components: {
- userFilter,
- milestoneFilter,
- labelFilter,
- },
- template: `
- <div class="modal-filters">
- <user-filter
- dropdown-class-name="dropdown-menu-author"
- toggle-class-name="js-user-search js-author-search"
- toggle-label="Author"
- field-name="author_id"
- :project-id="projectId"></user-filter>
- <user-filter
- dropdown-class-name="dropdown-menu-author"
- toggle-class-name="js-assignee-search"
- toggle-label="Assignee"
- field-name="assignee_id"
- :null-user="true"
- :project-id="projectId"></user-filter>
- <milestone-filter :milestone-path="milestonePath"></milestone-filter>
- <label-filter :label-path="labelPath"></label-filter>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
deleted file mode 100644
index 4fc8f72a145..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global LabelsSelect */
-module.exports = Vue.extend({
- name: 'filter-label',
- props: {
- labelPath: {
- type: String,
- required: true,
- },
- },
- mounted() {
- new LabelsSelect(this.$refs.dropdown);
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
- type="button"
- data-toggle="dropdown"
- data-show-any="true"
- data-show-no="true"
- :data-labels="labelPath"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- Label
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
- <div class="dropdown-title">
- Filter by label
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- placeholder="Search"
- autocomplete="off" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
deleted file mode 100644
index d555599d300..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
+++ /dev/null
@@ -1,55 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global MilestoneSelect */
-module.exports = Vue.extend({
- name: 'filter-milestone',
- props: {
- milestonePath: {
- type: String,
- required: true,
- },
- },
- mounted() {
- new MilestoneSelect(null, this.$refs.dropdown);
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-milestone-select"
- type="button"
- data-toggle="dropdown"
- data-show-any="true"
- data-show-upcoming="true"
- data-field-name="milestone_title"
- :data-milestones="milestonePath"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- Milestone
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
- <div class="dropdown-title">
- <span>Filter by milestone</span>
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- placeholder="Search milestones"
- autocomplete="off" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
deleted file mode 100644
index 8523028c29c..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global UsersSelect */
-module.exports = Vue.extend({
- name: 'filter-user',
- props: {
- toggleClassName: {
- type: String,
- required: true,
- },
- dropdownClassName: {
- type: String,
- required: false,
- default: '',
- },
- toggleLabel: {
- type: String,
- required: true,
- },
- fieldName: {
- type: String,
- required: true,
- },
- nullUser: {
- type: Boolean,
- required: false,
- default: false,
- },
- projectId: {
- type: Number,
- required: true,
- },
- },
- mounted() {
- new UsersSelect(null, this.$refs.dropdown);
- },
- computed: {
- currentUsername() {
- return gon.current_username;
- },
- dropdownTitle() {
- return `Filter by ${this.toggleLabel.toLowerCase()}`;
- },
- inputPlaceholder() {
- return `Search ${this.toggleLabel.toLowerCase()}`;
- },
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-user-search"
- :class="toggleClassName"
- type="button"
- data-toggle="dropdown"
- data-current-user="true"
- :data-any-user="'Any ' + toggleLabel"
- :data-null-user="nullUser"
- :data-field-name="fieldName"
- :data-project-id="projectId"
- :data-first-user="currentUsername"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- {{ toggleLabel }}
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div
- class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
- :class="dropdownClassName">
- <div class="dropdown-title">
- {{ dropdownTitle }}
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- autocomplete="off"
- :placeholder="inputPlaceholder" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i
- role="button"
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
- </i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js
index 1cbc422c961..1cbc422c961 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js.es6
+++ b/app/assets/javascripts/boards/components/modal/footer.js
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
new file mode 100644
index 00000000000..116e29cd177
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import modalFilters from './filters';
+
+require('./tabs');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalHeader = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ components: {
+ 'modal-tabs': gl.issueBoards.ModalTabs,
+ modalFilters,
+ },
+ template: `
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)">
+ <span aria-hidden="true">×</span>
+ </button>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
+ <div
+ class="add-issues-search append-bottom-10"
+ v-if="showSearch">
+ <modal-filters :store="filter" />
+ <button
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ ref="selectAllBtn"
+ @click="toggleAll">
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6
deleted file mode 100644
index 70c088f9054..00000000000
--- a/app/assets/javascripts/boards/components/modal/header.js.es6
+++ /dev/null
@@ -1,90 +0,0 @@
-/* global Vue */
-require('./tabs');
-const modalFilters = require('./filters');
-
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
-
- gl.issueBoards.ModalHeader = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
-
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
- },
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
-
- ModalStore.toggleAll();
- },
- },
- components: {
- 'modal-tabs': gl.issueBoards.ModalTabs,
- modalFilters,
- },
- template: `
- <div>
- <header class="add-issues-header form-actions">
- <h2>
- Add issues
- <button
- type="button"
- class="close"
- data-dismiss="modal"
- aria-label="Close"
- @click="toggleModal(false)">
- <span aria-hidden="true">×</span>
- </button>
- </h2>
- </header>
- <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
- <div
- class="add-issues-search append-bottom-10"
- v-if="showSearch">
- <modal-filters
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-filters>
- <input
- placeholder="Search issues..."
- class="form-control"
- type="search"
- v-model="searchTerm" />
- <button
- type="button"
- class="btn btn-success btn-inverted prepend-left-10"
- ref="selectAllBtn"
- @click="toggleAll">
- {{ selectAllText }}
- </button>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
new file mode 100644
index 00000000000..4240c97617d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -0,0 +1,159 @@
+/* global Vue */
+/* global ListIssue */
+import queryData from '../../utils/query_data';
+
+require('./header');
+require('./list');
+require('./footer');
+require('./empty_state');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.IssuesModal = Vue.extend({
+ props: {
+ blankStateImage: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+
+ this.loadIssues()
+ .then(() => {
+ this.loading = false;
+ });
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ this.page = 1;
+ this.loadIssues(true);
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
+
+ return gl.boardService.getBacklog(queryData(this.filter.path, {
+ page: this.page,
+ per: this.perPage,
+ })).then((res) => {
+ const data = res.json();
+
+ if (clearIssues) {
+ this.issues = [];
+ }
+
+ data.issues.forEach((issueObj) => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
+
+ this.issues.push(issue);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ });
+ },
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
+ },
+ },
+ created() {
+ this.page = 1;
+ },
+ components: {
+ 'modal-header': gl.issueBoards.ModalHeader,
+ 'modal-list': gl.issueBoards.ModalList,
+ 'modal-footer': gl.issueBoards.ModalFooter,
+ 'empty-state': gl.issueBoards.ModalEmptyState,
+ },
+ template: `
+ <div
+ class="add-issues-modal"
+ v-if="showAddIssuesModal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-header>
+ <modal-list
+ :image="blankStateImage"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ v-if="!loading && showList"></modal-list>
+ <empty-state
+ v-if="showEmptyState"
+ :image="blankStateImage"
+ :new-issue-path="newIssuePath"></empty-state>
+ <section
+ class="add-issues-list text-center"
+ v-if="loading">
+ <div class="add-issues-list-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </section>
+ <modal-footer></modal-footer>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6
deleted file mode 100644
index f290cd13763..00000000000
--- a/app/assets/javascripts/boards/components/modal/index.js.es6
+++ /dev/null
@@ -1,163 +0,0 @@
-/* global Vue */
-/* global ListIssue */
-
-require('./header');
-require('./list');
-require('./footer');
-require('./empty_state');
-
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
-
- gl.issueBoards.IssuesModal = Vue.extend({
- props: {
- blankStateImage: {
- type: String,
- required: true,
- },
- newIssuePath: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- watch: {
- page() {
- this.loadIssues();
- },
- searchTerm() {
- this.searchOperation();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
-
- this.loadIssues()
- .then(() => {
- this.loading = false;
- });
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
- }
- },
- filter: {
- handler() {
- this.loadIssues(true);
- },
- deep: true,
- },
- },
- methods: {
- searchOperation: _.debounce(function searchOperationDebounce() {
- this.loadIssues(true);
- }, 500),
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
-
- const queryData = Object.assign({}, this.filter, {
- search: this.searchTerm,
- page: this.page,
- per: this.perPage,
- });
-
- return gl.boardService.getBacklog(queryData).then((res) => {
- const data = res.json();
-
- if (clearIssues) {
- this.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
-
- this.issues.push(issue);
- });
-
- this.loadingNewPage = false;
-
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- });
- },
- },
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
-
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
-
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
- },
- components: {
- 'modal-header': gl.issueBoards.ModalHeader,
- 'modal-list': gl.issueBoards.ModalList,
- 'modal-footer': gl.issueBoards.ModalFooter,
- 'empty-state': gl.issueBoards.ModalEmptyState,
- },
- template: `
- <div
- class="add-issues-modal"
- v-if="showAddIssuesModal">
- <div class="add-issues-container">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-header>
- <modal-list
- :image="blankStateImage"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- v-if="!loading && showList"></modal-list>
- <empty-state
- v-if="showEmptyState"
- :image="blankStateImage"
- :new-issue-path="newIssuePath"></empty-state>
- <section
- class="add-issues-list text-center"
- v-if="loading">
- <div class="add-issues-list-loading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
- </section>
- <modal-footer></modal-footer>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js
index 3730c1ecaeb..3730c1ecaeb 100644
--- a/app/assets/javascripts/boards/components/modal/list.js.es6
+++ b/app/assets/javascripts/boards/components/modal/list.js
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 3c05120a2da..3c05120a2da 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
new file mode 100644
index 00000000000..1cd6ca0ee88
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -0,0 +1,47 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalTabs = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ template: `
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')">
+ Open issues
+ <span class="badge">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')">
+ Selected issues
+ <span class="badge">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6
deleted file mode 100644
index e8cb43f3503..00000000000
--- a/app/assets/javascripts/boards/components/modal/tabs.js.es6
+++ /dev/null
@@ -1,47 +0,0 @@
-/* global Vue */
-(() => {
- const ModalStore = gl.issueBoards.ModalStore;
-
- gl.issueBoards.ModalTabs = Vue.extend({
- mixins: [gl.issueBoards.ModalMixins],
- data() {
- return ModalStore.store;
- },
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- template: `
- <div class="top-area prepend-top-10 append-bottom-10">
- <ul class="nav-links issues-state-filters">
- <li :class="{ 'active': activeTab == 'all' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('all')">
- All issues
- <span class="badge">
- {{ issuesCount }}
- </span>
- </a>
- </li>
- <li :class="{ 'active': activeTab == 'selected' }">
- <a
- href="#"
- role="button"
- @click.prevent="changeTab('selected')">
- Selected issues
- <span class="badge">
- {{ selectedCount }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 556826a9148..556826a9148 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index e74935e1cb0..e74935e1cb0 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/boards/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
new file mode 100644
index 00000000000..101732309ea
--- /dev/null
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+import FilteredSearchContainer from '../filtered_search/container';
+
+export default class FilteredSearchBoards extends gl.FilteredSearchManager {
+ constructor(store, updateUrl = false) {
+ super('boards');
+
+ this.store = store;
+ this.updateUrl = updateUrl;
+
+ // Issue boards is slightly different, we handle all the requests async
+ // instead or reloading the page, we just re-fire the list ajax requests
+ this.isHandledAsync = true;
+ }
+
+ updateObject(path) {
+ this.store.path = path.substr(1);
+
+ if (this.updateUrl) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
+ }
+
+ removeTokens() {
+ const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
+
+ // Remove all the tokens as they will be replaced by the search manager
+ [].forEach.call(tokens, (el) => {
+ el.parentNode.removeChild(el);
+ });
+ }
+
+ updateTokens() {
+ this.removeTokens();
+
+ this.loadSearchParamsFromURL();
+
+ // Get the placeholder back if search is empty
+ this.filteredSearchInput.dispatchEvent(new Event('input'));
+ }
+}
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js
index 03425bb145b..03425bb145b 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js
index d378b7d4baf..d378b7d4baf 100644
--- a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index b6c6d17274f..b6c6d17274f 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
new file mode 100644
index 00000000000..ca5e6fa7e9d
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -0,0 +1,75 @@
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
+/* global Vue */
+/* global ListLabel */
+/* global ListMilestone */
+/* global ListUser */
+
+class ListIssue {
+ constructor (obj) {
+ this.globalId = obj.id;
+ this.id = obj.iid;
+ this.title = obj.title;
+ this.confidential = obj.confidential;
+ this.dueDate = obj.due_date;
+ this.subscribed = obj.subscribed;
+ this.labels = [];
+ this.selected = false;
+ this.assignee = false;
+ this.position = obj.relative_position || Infinity;
+
+ if (obj.assignee) {
+ this.assignee = new ListUser(obj.assignee);
+ }
+
+ if (obj.milestone) {
+ this.milestone = new ListMilestone(obj.milestone);
+ }
+
+ obj.labels.forEach((label) => {
+ this.labels.push(new ListLabel(label));
+ });
+ }
+
+ addLabel (label) {
+ if (!this.findLabel(label)) {
+ this.labels.push(new ListLabel(label));
+ }
+ }
+
+ findLabel (findLabel) {
+ return this.labels.filter(label => label.title === findLabel.title)[0];
+ }
+
+ removeLabel (removeLabel) {
+ if (removeLabel) {
+ this.labels = this.labels.filter(label => removeLabel.title !== label.title);
+ }
+ }
+
+ removeLabels (labels) {
+ labels.forEach(this.removeLabel.bind(this));
+ }
+
+ getLists () {
+ return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
+ }
+
+ update (url) {
+ const data = {
+ issue: {
+ milestone_id: this.milestone ? this.milestone.id : null,
+ due_date: this.dueDate,
+ assignee_id: this.assignee ? this.assignee.id : null,
+ label_ids: this.labels.map((label) => label.id)
+ }
+ };
+
+ if (!data.issue.label_ids.length) {
+ data.issue.label_ids = [''];
+ }
+
+ return Vue.http.patch(url, data);
+ }
+}
+
+window.ListIssue = ListIssue;
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
deleted file mode 100644
index 2d0a295ae4d..00000000000
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ /dev/null
@@ -1,78 +0,0 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
-/* global Vue */
-/* global ListLabel */
-/* global ListMilestone */
-/* global ListUser */
-
-class ListIssue {
- constructor (obj) {
- this.globalId = obj.id;
- this.id = obj.iid;
- this.title = obj.title;
- this.confidential = obj.confidential;
- this.dueDate = obj.due_date;
- this.subscribed = obj.subscribed;
- this.labels = [];
- this.selected = false;
- this.assignee = false;
-
- if (obj.assignee) {
- this.assignee = new ListUser(obj.assignee);
- }
-
- if (obj.milestone) {
- this.milestone = new ListMilestone(obj.milestone);
- }
-
- obj.labels.forEach((label) => {
- this.labels.push(new ListLabel(label));
- });
-
- this.priority = this.labels.reduce((max, label) => {
- return (label.priority < max) ? label.priority : max;
- }, Infinity);
- }
-
- addLabel (label) {
- if (!this.findLabel(label)) {
- this.labels.push(new ListLabel(label));
- }
- }
-
- findLabel (findLabel) {
- return this.labels.filter(label => label.title === findLabel.title)[0];
- }
-
- removeLabel (removeLabel) {
- if (removeLabel) {
- this.labels = this.labels.filter(label => removeLabel.title !== label.title);
- }
- }
-
- removeLabels (labels) {
- labels.forEach(this.removeLabel.bind(this));
- }
-
- getLists () {
- return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
- }
-
- update (url) {
- const data = {
- issue: {
- milestone_id: this.milestone ? this.milestone.id : null,
- due_date: this.dueDate,
- assignee_id: this.assignee ? this.assignee.id : null,
- label_ids: this.labels.map((label) => label.id)
- }
- };
-
- if (!data.issue.label_ids.length) {
- data.issue.label_ids = [''];
- }
-
- return Vue.http.patch(url, data);
- }
-}
-
-window.ListIssue = ListIssue;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js
index 9af88d167d6..9af88d167d6 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
new file mode 100644
index 00000000000..f18ad2a0fac
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js
@@ -0,0 +1,172 @@
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* global ListIssue */
+/* global ListLabel */
+import queryData from '../utils/query_data';
+
+class List {
+ constructor (obj) {
+ this.id = obj.id;
+ this._uid = this.guid();
+ this.position = obj.position;
+ this.title = obj.title;
+ this.type = obj.list_type;
+ this.preset = ['done', 'blank'].indexOf(this.type) > -1;
+ this.page = 1;
+ this.loading = true;
+ this.loadingMore = false;
+ this.issues = [];
+ this.issuesSize = 0;
+
+ if (obj.label) {
+ this.label = new ListLabel(obj.label);
+ }
+
+ if (this.type !== 'blank' && this.id) {
+ this.getIssues();
+ }
+ }
+
+ guid() {
+ const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
+ }
+
+ save () {
+ return gl.boardService.createList(this.label.id)
+ .then((resp) => {
+ const data = resp.json();
+
+ this.id = data.id;
+ this.type = data.list_type;
+ this.position = data.position;
+
+ return this.getIssues();
+ });
+ }
+
+ destroy () {
+ const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
+ gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
+ gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
+
+ gl.boardService.destroyList(this.id);
+ }
+
+ update () {
+ gl.boardService.updateList(this.id, this.position);
+ }
+
+ nextPage () {
+ if (this.issuesSize > this.issues.length) {
+ this.page += 1;
+
+ return this.getIssues(false);
+ }
+ }
+
+ getIssues (emptyIssues = true) {
+ const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
+
+ if (this.label && data.label_name) {
+ data.label_name = data.label_name.filter(label => label !== this.label.title);
+ }
+
+ if (emptyIssues) {
+ this.loading = true;
+ }
+
+ return gl.boardService.getIssuesForList(this.id, data)
+ .then((resp) => {
+ const data = resp.json();
+ this.loading = false;
+ this.issuesSize = data.size;
+
+ if (emptyIssues) {
+ this.issues = [];
+ }
+
+ this.createIssues(data.issues);
+ });
+ }
+
+ newIssue (issue) {
+ this.addIssue(issue);
+ this.issuesSize += 1;
+
+ return gl.boardService.newIssue(this.id, issue)
+ .then((resp) => {
+ const data = resp.json();
+ issue.id = data.iid;
+ });
+ }
+
+ createIssues (data) {
+ data.forEach((issueObj) => {
+ this.addIssue(new ListIssue(issueObj));
+ });
+ }
+
+ addIssue (issue, listFrom, newIndex) {
+ let moveBeforeIid = null;
+ let moveAfterIid = null;
+
+ if (!this.findIssue(issue.id)) {
+ if (newIndex !== undefined) {
+ this.issues.splice(newIndex, 0, issue);
+
+ if (this.issues[newIndex - 1]) {
+ moveBeforeIid = this.issues[newIndex - 1].id;
+ }
+
+ if (this.issues[newIndex + 1]) {
+ moveAfterIid = this.issues[newIndex + 1].id;
+ }
+ } else {
+ this.issues.push(issue);
+ }
+
+ if (this.label) {
+ issue.addLabel(this.label);
+ }
+
+ if (listFrom) {
+ this.issuesSize += 1;
+
+ this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
+ }
+ }
+ }
+
+ moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
+ this.issues.splice(oldIndex, 1);
+ this.issues.splice(newIndex, 0, issue);
+
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid);
+ }
+
+ updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
+ .then(() => {
+ listFrom.getIssues(false);
+ });
+ }
+
+ findIssue (id) {
+ return this.issues.filter(issue => issue.id === id)[0];
+ }
+
+ removeIssue (removeIssue) {
+ this.issues = this.issues.filter((issue) => {
+ const matchesRemove = removeIssue.id === issue.id;
+
+ if (matchesRemove) {
+ this.issuesSize -= 1;
+ issue.removeLabel(this.label);
+ }
+
+ return !matchesRemove;
+ });
+ }
+}
+
+window.List = List;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
deleted file mode 100644
index 5152be56b66..00000000000
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ /dev/null
@@ -1,152 +0,0 @@
-/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
-/* global ListIssue */
-/* global ListLabel */
-
-class List {
- constructor (obj) {
- this.id = obj.id;
- this._uid = this.guid();
- this.position = obj.position;
- this.title = obj.title;
- this.type = obj.list_type;
- this.preset = ['done', 'blank'].indexOf(this.type) > -1;
- this.filters = gl.issueBoards.BoardsStore.state.filters;
- this.page = 1;
- this.loading = true;
- this.loadingMore = false;
- this.issues = [];
- this.issuesSize = 0;
-
- if (obj.label) {
- this.label = new ListLabel(obj.label);
- }
-
- if (this.type !== 'blank' && this.id) {
- this.getIssues();
- }
- }
-
- guid() {
- const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
- return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
- }
-
- save () {
- return gl.boardService.createList(this.label.id)
- .then((resp) => {
- const data = resp.json();
-
- this.id = data.id;
- this.type = data.list_type;
- this.position = data.position;
-
- return this.getIssues();
- });
- }
-
- destroy () {
- const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
- gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
- gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
-
- gl.boardService.destroyList(this.id);
- }
-
- update () {
- gl.boardService.updateList(this.id, this.position);
- }
-
- nextPage () {
- if (this.issuesSize > this.issues.length) {
- this.page += 1;
-
- return this.getIssues(false);
- }
- }
-
- getIssues (emptyIssues = true) {
- const filters = this.filters;
- const data = { page: this.page };
-
- Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
-
- if (this.label) {
- data.label_name = data.label_name.filter(label => label !== this.label.title);
- }
-
- if (emptyIssues) {
- this.loading = true;
- }
-
- return gl.boardService.getIssuesForList(this.id, data)
- .then((resp) => {
- const data = resp.json();
- this.loading = false;
- this.issuesSize = data.size;
-
- if (emptyIssues) {
- this.issues = [];
- }
-
- this.createIssues(data.issues);
- });
- }
-
- newIssue (issue) {
- this.addIssue(issue);
- this.issuesSize += 1;
-
- return gl.boardService.newIssue(this.id, issue)
- .then((resp) => {
- const data = resp.json();
- issue.id = data.iid;
- });
- }
-
- createIssues (data) {
- data.forEach((issueObj) => {
- this.addIssue(new ListIssue(issueObj));
- });
- }
-
- addIssue (issue, listFrom, newIndex) {
- if (!this.findIssue(issue.id)) {
- if (newIndex !== undefined) {
- this.issues.splice(newIndex, 0, issue);
- } else {
- this.issues.push(issue);
- }
-
- if (this.label) {
- issue.addLabel(this.label);
- }
-
- if (listFrom) {
- this.issuesSize += 1;
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
- .then(() => {
- listFrom.getIssues(false);
- });
- }
- }
- }
-
- findIssue (id) {
- return this.issues.filter(issue => issue.id === id)[0];
- }
-
- removeIssue (removeIssue) {
- this.issues = this.issues.filter((issue) => {
- const matchesRemove = removeIssue.id === issue.id;
-
- if (matchesRemove) {
- this.issuesSize -= 1;
- issue.removeLabel(this.label);
- }
-
- return !matchesRemove;
- });
- }
-}
-
-window.List = List;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js
index c867b06d320..c867b06d320 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js
index 8e9de4d4cbb..8e9de4d4cbb 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
new file mode 100644
index 00000000000..e54102814d6
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -0,0 +1,97 @@
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
+/* global Vue */
+
+class BoardService {
+ constructor (root, bulkUpdatePath, boardId) {
+ this.boards = Vue.resource(`${root}{/id}.json`, {}, {
+ issues: {
+ method: 'GET',
+ url: `${root}/${boardId}/issues.json`
+ }
+ });
+ this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
+ generate: {
+ method: 'POST',
+ url: `${root}/${boardId}/lists/generate.json`
+ }
+ });
+ this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
+ bulkUpdate: {
+ method: 'POST',
+ url: bulkUpdatePath,
+ },
+ });
+
+ Vue.http.interceptors.push((request, next) => {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ next();
+ });
+ }
+
+ all () {
+ return this.lists.get();
+ }
+
+ generateDefaultLists () {
+ return this.lists.generate({});
+ }
+
+ createList (label_id) {
+ return this.lists.save({}, {
+ list: {
+ label_id
+ }
+ });
+ }
+
+ updateList (id, position) {
+ return this.lists.update({ id }, {
+ list: {
+ position
+ }
+ });
+ }
+
+ destroyList (id) {
+ return this.lists.delete({ id });
+ }
+
+ getIssuesForList (id, filter = {}) {
+ const data = { id };
+ Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+
+ return this.issues.get(data);
+ }
+
+ moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
+ return this.issue.update({ id }, {
+ from_list_id,
+ to_list_id,
+ move_before_iid,
+ move_after_iid,
+ });
+ }
+
+ newIssue (id, issue) {
+ return this.issues.save({ id }, {
+ issue
+ });
+ }
+
+ getBacklog(data) {
+ return this.boards.issues(data);
+ }
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return this.issues.bulkUpdate(data);
+ }
+}
+
+window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
deleted file mode 100644
index 065e90518df..00000000000
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ /dev/null
@@ -1,95 +0,0 @@
-/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
-/* global Vue */
-
-class BoardService {
- constructor (root, bulkUpdatePath, boardId) {
- this.boards = Vue.resource(`${root}{/id}.json`, {}, {
- issues: {
- method: 'GET',
- url: `${root}/${boardId}/issues.json`
- }
- });
- this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
- generate: {
- method: 'POST',
- url: `${root}/${boardId}/lists/generate.json`
- }
- });
- this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
- bulkUpdate: {
- method: 'POST',
- url: bulkUpdatePath,
- },
- });
-
- Vue.http.interceptors.push((request, next) => {
- request.headers['X-CSRF-Token'] = $.rails.csrfToken();
- next();
- });
- }
-
- all () {
- return this.lists.get();
- }
-
- generateDefaultLists () {
- return this.lists.generate({});
- }
-
- createList (label_id) {
- return this.lists.save({}, {
- list: {
- label_id
- }
- });
- }
-
- updateList (id, position) {
- return this.lists.update({ id }, {
- list: {
- position
- }
- });
- }
-
- destroyList (id) {
- return this.lists.delete({ id });
- }
-
- getIssuesForList (id, filter = {}) {
- const data = { id };
- Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
-
- return this.issues.get(data);
- }
-
- moveIssue (id, from_list_id, to_list_id) {
- return this.issue.update({ id }, {
- from_list_id,
- to_list_id
- });
- }
-
- newIssue (id, issue) {
- return this.issues.save({ id }, {
- issue
- });
- }
-
- getBacklog(data) {
- return this.boards.issues(data);
- }
-
- bulkUpdate(issueIds, extraData = {}) {
- const data = {
- update: Object.assign(extraData, {
- issuable_ids: issueIds.join(','),
- }),
- };
-
- return this.issues.bulkUpdate(data);
- }
-}
-
-window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
new file mode 100644
index 00000000000..28ecb322df7
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -0,0 +1,126 @@
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
+/* global Cookies */
+/* global List */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardsStore = {
+ disabled: false,
+ filter: {
+ path: '',
+ },
+ state: {},
+ detail: {
+ issue: {}
+ },
+ moving: {
+ issue: {},
+ list: {}
+ },
+ create () {
+ this.state.lists = [];
+ this.filter.path = gl.utils.getUrlParamsArray().join('&');
+ },
+ addList (listObj) {
+ const list = new List(listObj);
+ this.state.lists.push(list);
+
+ return list;
+ },
+ new (listObj) {
+ const list = this.addList(listObj);
+
+ list
+ .save()
+ .then(() => {
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+ });
+ this.removeBlankState();
+ },
+ updateNewListDropdown (listId) {
+ $(`.js-board-list-${listId}`).removeClass('is-active');
+ },
+ shouldAddBlankState () {
+ // Decide whether to add the blank state
+ return !(this.state.lists.filter(list => list.type !== 'done')[0]);
+ },
+ addBlankState () {
+ if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
+
+ this.addList({
+ id: 'blank',
+ list_type: 'blank',
+ title: 'Welcome to your Issue Board!',
+ position: 0
+ });
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+ },
+ removeBlankState () {
+ this.removeList('blank');
+
+ Cookies.set('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10,
+ path: ''
+ });
+ },
+ welcomeIsHidden () {
+ return Cookies.get('issue_board_welcome_hidden') === 'true';
+ },
+ removeList (id, type = 'blank') {
+ const list = this.findList('id', id, type);
+
+ if (!list) return;
+
+ this.state.lists = this.state.lists.filter(list => list.id !== id);
+ },
+ moveList (listFrom, orderLists) {
+ orderLists.forEach((id, i) => {
+ const list = this.findList('id', parseInt(id, 10));
+
+ list.position = i;
+ });
+ listFrom.update();
+ },
+ moveIssueToList (listFrom, listTo, issue, newIndex) {
+ const issueTo = listTo.findIssue(issue.id);
+ const issueLists = issue.getLists();
+ const listLabels = issueLists.map(listIssue => listIssue.label);
+
+ if (!issueTo) {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ } else {
+ listTo.updateIssueLabel(issue, listFrom);
+ issueTo.removeLabel(listFrom.label);
+ }
+
+ if (listTo.type === 'done') {
+ issueLists.forEach((list) => {
+ list.removeIssue(issue);
+ });
+ issue.removeLabels(listLabels);
+ } else {
+ listFrom.removeIssue(issue);
+ }
+ },
+ moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
+ const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+
+ list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
+ },
+ findList (key, val, type = 'label') {
+ return this.state.lists.filter((list) => {
+ const byType = type ? list['type'] === type : true;
+
+ return list[key] === val && byType;
+ })[0];
+ },
+ updateFiltersUrl () {
+ history.pushState(null, null, `?${this.filter.path}`);
+ }
+ };
+})();
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
deleted file mode 100644
index 50842ecbaaa..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ /dev/null
@@ -1,120 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
-/* global Cookies */
-/* global List */
-
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardsStore = {
- disabled: false,
- state: {},
- detail: {
- issue: {}
- },
- moving: {
- issue: {},
- list: {}
- },
- create () {
- this.state.lists = [];
- this.state.filters = {
- author_id: gl.utils.getParameterValues('author_id')[0],
- assignee_id: gl.utils.getParameterValues('assignee_id')[0],
- milestone_title: gl.utils.getParameterValues('milestone_title')[0],
- label_name: gl.utils.getParameterValues('label_name[]'),
- search: ''
- };
- },
- addList (listObj) {
- const list = new List(listObj);
- this.state.lists.push(list);
-
- return list;
- },
- new (listObj) {
- const list = this.addList(listObj);
-
- list
- .save()
- .then(() => {
- this.state.lists = _.sortBy(this.state.lists, 'position');
- });
- this.removeBlankState();
- },
- updateNewListDropdown (listId) {
- $(`.js-board-list-${listId}`).removeClass('is-active');
- },
- shouldAddBlankState () {
- // Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'done')[0]);
- },
- addBlankState () {
- if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
-
- this.addList({
- id: 'blank',
- list_type: 'blank',
- title: 'Welcome to your Issue Board!',
- position: 0
- });
-
- this.state.lists = _.sortBy(this.state.lists, 'position');
- },
- removeBlankState () {
- this.removeList('blank');
-
- Cookies.set('issue_board_welcome_hidden', 'true', {
- expires: 365 * 10,
- path: ''
- });
- },
- welcomeIsHidden () {
- return Cookies.get('issue_board_welcome_hidden') === 'true';
- },
- removeList (id, type = 'blank') {
- const list = this.findList('id', id, type);
-
- if (!list) return;
-
- this.state.lists = this.state.lists.filter(list => list.id !== id);
- },
- moveList (listFrom, orderLists) {
- orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id, 10));
-
- list.position = i;
- });
- listFrom.update();
- },
- moveIssueToList (listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id);
- const issueLists = issue.getLists();
- const listLabels = issueLists.map(listIssue => listIssue.label);
-
- // Add to new lists issues if it doesn't already exist
- if (!issueTo) {
- listTo.addIssue(issue, listFrom, newIndex);
- }
-
- if (listTo.type === 'done') {
- issueLists.forEach((list) => {
- list.removeIssue(issue);
- });
- issue.removeLabels(listLabels);
- } else {
- listFrom.removeIssue(issue);
- }
- },
- findList (key, val, type = 'label') {
- return this.state.lists.filter((list) => {
- const byType = type ? list['type'] === type : true;
-
- return list[key] === val && byType;
- })[0];
- },
- updateFiltersUrl () {
- history.pushState(null, null, `?${$.param(this.state.filters)}`);
- }
- };
-})();
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
new file mode 100644
index 00000000000..7ee266a831f
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -0,0 +1,99 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ class ModalStore {
+ constructor() {
+ this.store = {
+ columns: 3,
+ issues: [],
+ issuesCount: false,
+ selectedIssues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ selectedList: null,
+ searchTerm: '',
+ loading: false,
+ loadingNewPage: false,
+ page: 1,
+ perPage: 50,
+ filter: {
+ path: '',
+ },
+ };
+ }
+
+ selectedCount() {
+ return this.getSelectedIssues().length;
+ }
+
+ toggleIssue(issueObj) {
+ const issue = issueObj;
+ const selected = issue.selected;
+
+ issue.selected = !selected;
+
+ if (!selected) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+
+ toggleAll() {
+ const select = this.selectedCount() !== this.store.issues.length;
+
+ this.store.issues.forEach((issue) => {
+ const issueUpdate = issue;
+
+ if (issueUpdate.selected !== select) {
+ issueUpdate.selected = select;
+
+ if (select) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+ });
+ }
+
+ getSelectedIssues() {
+ return this.store.selectedIssues.filter(issue => issue.selected);
+ }
+
+ addSelectedIssue(issue) {
+ const index = this.selectedIssueIndex(issue);
+
+ if (index === -1) {
+ this.store.selectedIssues.push(issue);
+ }
+ }
+
+ removeSelectedIssue(issue, forcePurge = false) {
+ if (this.store.activeTab === 'all' || forcePurge) {
+ this.store.selectedIssues = this.store.selectedIssues
+ .filter(fIssue => fIssue.id !== issue.id);
+ }
+ }
+
+ purgeUnselectedIssues() {
+ this.store.selectedIssues.forEach((issue) => {
+ if (!issue.selected) {
+ this.removeSelectedIssue(issue, true);
+ }
+ });
+ }
+
+ selectedIssueIndex(issue) {
+ return this.store.selectedIssues.indexOf(issue);
+ }
+
+ findSelectedIssue(issue) {
+ return this.store.selectedIssues
+ .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ }
+ }
+
+ gl.issueBoards.ModalStore = new ModalStore();
+})();
diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6
deleted file mode 100644
index 15fc6c79e8d..00000000000
--- a/app/assets/javascripts/boards/stores/modal_store.js.es6
+++ /dev/null
@@ -1,107 +0,0 @@
-(() => {
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- class ModalStore {
- constructor() {
- this.store = {
- columns: 3,
- issues: [],
- issuesCount: false,
- selectedIssues: [],
- showAddIssuesModal: false,
- activeTab: 'all',
- selectedList: null,
- searchTerm: '',
- loading: false,
- loadingNewPage: false,
- page: 1,
- perPage: 50,
- };
-
- this.setDefaultFilter();
- }
-
- setDefaultFilter() {
- this.store.filter = {
- author_id: '',
- assignee_id: '',
- milestone_title: '',
- label_name: [],
- };
- }
-
- selectedCount() {
- return this.getSelectedIssues().length;
- }
-
- toggleIssue(issueObj) {
- const issue = issueObj;
- const selected = issue.selected;
-
- issue.selected = !selected;
-
- if (!selected) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
- }
-
- toggleAll() {
- const select = this.selectedCount() !== this.store.issues.length;
-
- this.store.issues.forEach((issue) => {
- const issueUpdate = issue;
-
- if (issueUpdate.selected !== select) {
- issueUpdate.selected = select;
-
- if (select) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
- }
- });
- }
-
- getSelectedIssues() {
- return this.store.selectedIssues.filter(issue => issue.selected);
- }
-
- addSelectedIssue(issue) {
- const index = this.selectedIssueIndex(issue);
-
- if (index === -1) {
- this.store.selectedIssues.push(issue);
- }
- }
-
- removeSelectedIssue(issue, forcePurge = false) {
- if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues
- .filter(fIssue => fIssue.id !== issue.id);
- }
- }
-
- purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
- if (!issue.selected) {
- this.removeSelectedIssue(issue, true);
- }
- });
- }
-
- selectedIssueIndex(issue) {
- return this.store.selectedIssues.indexOf(issue);
- }
-
- findSelectedIssue(issue) {
- return this.store.selectedIssues
- .filter(filteredIssue => filteredIssue.id === issue.id)[0];
- }
- }
-
- gl.issueBoards.ModalStore = new ModalStore();
-})();
diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js
new file mode 100644
index 00000000000..2cd3c146f11
--- /dev/null
+++ b/app/assets/javascripts/boards/utils/query_data.js
@@ -0,0 +1,21 @@
+export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
+ if (filterParam === '') return dataParam;
+
+ const data = dataParam;
+ const paramSplit = filterParam.split('=');
+ const paramKeyNormalized = paramSplit[0].replace('[]', '');
+ const isArray = paramSplit[0].indexOf('[]');
+ const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
+
+ if (isArray !== -1) {
+ if (!data[paramKeyNormalized]) {
+ data[paramKeyNormalized] = [];
+ }
+
+ data[paramKeyNormalized].push(value);
+ } else {
+ data[paramKeyNormalized] = value;
+ }
+
+ return data;
+}, extraData);
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 22e93328548..2c1f988d987 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,72 +1,66 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
-(function() {
- var Breakpoints = (function() {
- var BreakpointInstance, instance;
+var Breakpoints = (function() {
+ var BreakpointInstance, instance;
- function Breakpoints() {}
+ function Breakpoints() {}
- instance = null;
+ instance = null;
- BreakpointInstance = (function() {
- var BREAKPOINTS;
+ BreakpointInstance = (function() {
+ var BREAKPOINTS;
- BREAKPOINTS = ["xs", "sm", "md", "lg"];
+ BREAKPOINTS = ["xs", "sm", "md", "lg"];
- function BreakpointInstance() {
- this.setup();
- }
-
- BreakpointInstance.prototype.setup = function() {
- var allDeviceSelector, els;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- if ($(allDeviceSelector.join(",")).length) {
- return;
- }
- // Create all the elements
- els = $.map(BREAKPOINTS, function(breakpoint) {
- return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
- });
- return $("body").append(els.join(''));
- };
+ function BreakpointInstance() {
+ this.setup();
+ }
- BreakpointInstance.prototype.visibleDevice = function() {
- var allDeviceSelector;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- return $(allDeviceSelector.join(",")).filter(":visible");
- };
-
- BreakpointInstance.prototype.getBreakpointSize = function() {
- var $visibleDevice;
- $visibleDevice = this.visibleDevice;
- // TODO: Consider refactoring in light of turbolinks removal.
- // the page refreshed via turbolinks
- if (!$visibleDevice().length) {
- this.setup();
- }
- $visibleDevice = this.visibleDevice();
- return $visibleDevice.attr("class").split("visible-")[1];
- };
+ BreakpointInstance.prototype.setup = function() {
+ var allDeviceSelector, els;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ if ($(allDeviceSelector.join(",")).length) {
+ return;
+ }
+ // Create all the elements
+ els = $.map(BREAKPOINTS, function(breakpoint) {
+ return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
+ });
+ return $("body").append(els.join(''));
+ };
- return BreakpointInstance;
- })();
+ BreakpointInstance.prototype.visibleDevice = function() {
+ var allDeviceSelector;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ return $(allDeviceSelector.join(",")).filter(":visible");
+ };
- Breakpoints.get = function() {
- return instance != null ? instance : instance = new BreakpointInstance;
+ BreakpointInstance.prototype.getBreakpointSize = function() {
+ var $visibleDevice;
+ $visibleDevice = this.visibleDevice;
+ // TODO: Consider refactoring in light of turbolinks removal.
+ // the page refreshed via turbolinks
+ if (!$visibleDevice().length) {
+ this.setup();
+ }
+ $visibleDevice = this.visibleDevice();
+ return $visibleDevice.attr("class").split("visible-")[1];
};
- return Breakpoints;
+ return BreakpointInstance;
})();
- $((function(_this) {
- return function() {
- return _this.bp = Breakpoints.get();
- };
- })(this));
+ Breakpoints.get = function() {
+ return instance != null ? instance : instance = new BreakpointInstance;
+ };
+
+ return Breakpoints;
+})();
+
+$(() => { window.bp = Breakpoints.get(); });
- window.Breakpoints = Breakpoints;
-}).call(window);
+window.Breakpoints = Breakpoints;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index e8531c43b4b..f73e489e7b2 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,34 +1,33 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
-(function() {
- $(function() {
- var previewPath;
- $('input#broadcast_message_color').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('background-color', previewColor);
- });
- $('input#broadcast_message_font').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('color', previewColor);
- });
- previewPath = $('textarea#broadcast_message_message').data('preview-path');
- return $('textarea#broadcast_message_message').on('input', function() {
- var message;
- message = $(this).val();
- if (message === '') {
- return $('.js-broadcast-message-preview').text("Your message here");
- } else {
- return $.ajax({
- url: previewPath,
- type: "POST",
- data: {
- broadcast_message: {
- message: message
- }
+
+$(function() {
+ var previewPath;
+ $('input#broadcast_message_color').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('background-color', previewColor);
+ });
+ $('input#broadcast_message_font').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('color', previewColor);
+ });
+ previewPath = $('textarea#broadcast_message_message').data('preview-path');
+ return $('textarea#broadcast_message_message').on('input', function() {
+ var message;
+ message = $(this).val();
+ if (message === '') {
+ return $('.js-broadcast-message-preview').text("Your message here");
+ } else {
+ return $.ajax({
+ url: previewPath,
+ type: "POST",
+ data: {
+ broadcast_message: {
+ message: message
}
- });
- }
- });
+ }
+ });
+ }
});
-}).call(window);
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 8fa1aceddff..6efd26ccc37 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,278 +1,283 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- var AUTO_SCROLL_OFFSET = 75;
- var DOWN_BUILD_TRACE = '#down-build-trace';
-
- this.Build = (function() {
- Build.interval = null;
-
- Build.state = null;
-
- function Build(options) {
- options = options || $('.js-build-options').data();
- this.pageUrl = options.pageUrl;
- this.buildUrl = options.buildUrl;
- this.buildStatus = options.buildStatus;
- this.state = options.logState;
- this.buildStage = options.buildStage;
- this.updateDropdown = bind(this.updateDropdown, this);
- this.$document = $(document);
- this.$body = $('body');
- this.$buildTrace = $('#build-trace');
- this.$autoScrollContainer = $('.autoscroll-container');
- this.$autoScrollStatus = $('#autoscroll-status');
- this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
- this.$upBuildTrace = $('#up-build-trace');
- this.$downBuildTrace = $(DOWN_BUILD_TRACE);
- this.$scrollTopBtn = $('#scroll-top');
- this.$scrollBottomBtn = $('#scroll-bottom');
- this.$buildRefreshAnimation = $('.js-build-refresh');
-
- clearInterval(Build.interval);
- // Init breakpoint checker
- this.bp = Breakpoints.get();
-
- this.initSidebar();
- this.$buildScroll = $('#js-build-scroll');
-
- this.populateJobs(this.buildStage);
- this.updateStageDropdownText(this.buildStage);
- this.sidebarOnResize();
-
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
- this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
- this.$document.on('scroll', this.initScrollMonitor.bind(this));
- $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
- $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
- this.updateArtifactRemoveDate();
- if ($('#build-trace').length) {
- this.getInitialBuildTrace();
- this.initScrollButtonAffix();
- }
- if (this.buildStatus === "running" || this.buildStatus === "pending") {
- Build.interval = setInterval((function(_this) {
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- return function() {
- if (_this.location() === _this.pageUrl) {
- return _this.getBuildTrace();
- }
- };
- })(this), 4000);
- }
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+var AUTO_SCROLL_OFFSET = 75;
+var DOWN_BUILD_TRACE = '#down-build-trace';
+
+window.Build = (function() {
+ Build.timeout = null;
+
+ Build.state = null;
+
+ function Build(options) {
+ options = options || $('.js-build-options').data();
+ this.pageUrl = options.pageUrl;
+ this.buildUrl = options.buildUrl;
+ this.buildStatus = options.buildStatus;
+ this.state = options.logState;
+ this.buildStage = options.buildStage;
+ this.updateDropdown = bind(this.updateDropdown, this);
+ this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $(DOWN_BUILD_TRACE);
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
+ clearTimeout(Build.timeout);
+ // Init breakpoint checker
+ this.bp = Breakpoints.get();
+
+ this.initSidebar();
+ this.$buildScroll = $('#js-build-scroll');
+
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
+
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
+ this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
+ $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+ $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
+ this.updateArtifactRemoveDate();
+ if ($('#build-trace').length) {
+ this.getInitialBuildTrace();
+ this.initScrollButtonAffix();
}
-
- Build.prototype.initSidebar = function() {
- this.$sidebar = $('.js-build-sidebar');
- this.$sidebar.niceScroll();
- this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
- };
-
- Build.prototype.location = function() {
- return window.location.href.split("#")[0];
- };
-
- Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
-
- return $.ajax({
- url: this.buildUrl,
- dataType: 'json',
- success: function(buildData) {
- $('.js-build-output').html(buildData.trace_html);
- if (window.location.hash === DOWN_BUILD_TRACE) {
- $("html,body").scrollTop(this.$buildTrace.height());
+ this.invokeBuildTrace();
+ }
+
+ Build.prototype.initSidebar = function() {
+ this.$sidebar = $('.js-build-sidebar');
+ this.$sidebar.niceScroll();
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ };
+
+ Build.prototype.location = function() {
+ return window.location.href.split("#")[0];
+ };
+
+ Build.prototype.invokeBuildTrace = function() {
+ var continueRefreshStatuses = ['running', 'pending'];
+ // Continue to update build trace when build is running or pending
+ if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
+ Build.timeout = setTimeout((function(_this) {
+ return function() {
+ if (_this.location() === _this.pageUrl) {
+ return _this.getBuildTrace();
}
- if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
- this.$buildRefreshAnimation.remove();
- return this.initScrollMonitor();
+ };
+ })(this), 4000);
+ }
+ };
+
+ Build.prototype.getInitialBuildTrace = function() {
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
+
+ return $.ajax({
+ url: this.buildUrl,
+ dataType: 'json',
+ success: function(buildData) {
+ $('.js-build-output').html(buildData.trace_html);
+ if (window.location.hash === DOWN_BUILD_TRACE) {
+ $("html,body").scrollTop(this.$buildTrace.height());
+ }
+ if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
+ this.$buildRefreshAnimation.remove();
+ return this.initScrollMonitor();
+ }
+ }.bind(this)
+ });
+ };
+
+ Build.prototype.getBuildTrace = function() {
+ return $.ajax({
+ url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ dataType: "json",
+ success: (function(_this) {
+ return function(log) {
+ var pageUrl;
+
+ if (log.state) {
+ _this.state = log.state;
}
- }.bind(this)
- });
- };
-
- Build.prototype.getBuildTrace = function() {
- return $.ajax({
- url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
- dataType: "json",
- success: (function(_this) {
- return function(log) {
- var pageUrl;
-
- if (log.state) {
- _this.state = log.state;
+ _this.invokeBuildTrace();
+ if (log.status === "running") {
+ if (log.append) {
+ $('.js-build-output').append(log.html);
+ } else {
+ $('.js-build-output').html(log.html);
}
- if (log.status === "running") {
- if (log.append) {
- $('.js-build-output').append(log.html);
- } else {
- $('.js-build-output').html(log.html);
- }
- return _this.checkAutoscroll();
- } else if (log.status !== _this.buildStatus) {
- pageUrl = _this.pageUrl;
- if (_this.$autoScrollStatus.data('state') === 'enabled') {
- pageUrl += DOWN_BUILD_TRACE;
- }
-
- return gl.utils.visitUrl(pageUrl);
+ return _this.checkAutoscroll();
+ } else if (log.status !== _this.buildStatus) {
+ pageUrl = _this.pageUrl;
+ if (_this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
}
- };
- })(this)
- });
- };
-
- Build.prototype.checkAutoscroll = function() {
- if (this.$autoScrollStatus.data("state") === "enabled") {
- return $("html,body").scrollTop(this.$buildTrace.height());
- }
-
- // Handle a situation where user started new build
- // but never scrolled a page
- if (!this.$scrollTopBtn.is(':visible') &&
- !this.$scrollBottomBtn.is(':visible') &&
- !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- this.$scrollBottomBtn.show();
- }
- };
- Build.prototype.initScrollButtonAffix = function() {
- // Hide everything initially
- this.$scrollTopBtn.hide();
- this.$scrollBottomBtn.hide();
- this.$autoScrollContainer.hide();
- };
-
- // Page scroll listener to detect if user has scrolling page
- // and handle following cases
- // 1) User is at Top of Build Log;
- // - Hide Top Arrow button
- // - Show Bottom Arrow button
- // - Disable Autoscroll and hide indicator (when build is running)
- // 2) User is at Bottom of Build Log;
- // - Show Top Arrow button
- // - Hide Bottom Arrow button
- // - Enable Autoscroll and show indicator (when build is running)
- // 3) User is somewhere in middle of Build Log;
- // - Show Top Arrow button
- // - Show Bottom Arrow button
- // - Disable Autoscroll and hide indicator (when build is running)
- Build.prototype.initScrollMonitor = function() {
- if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // User is somewhere in middle of Build Log
-
- this.$scrollTopBtn.show();
-
- if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
- this.$scrollBottomBtn.show();
- } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
- this.$scrollBottomBtn.show();
- } else {
- this.$scrollBottomBtn.hide();
- }
-
- // Hide Autoscroll Status Indicator
- if (this.$scrollBottomBtn.is(':visible')) {
- this.$autoScrollContainer.hide();
- this.$autoScrollStatusText.removeClass('animate');
- } else {
- this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
- this.$autoScrollStatusText.addClass('animate');
- }
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // User is at Top of Build Log
+ return gl.utils.visitUrl(pageUrl);
+ }
+ };
+ })(this)
+ });
+ };
+
+ Build.prototype.checkAutoscroll = function() {
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
- this.$scrollTopBtn.hide();
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
+ }
+ };
+
+ Build.prototype.initScrollButtonAffix = function() {
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ };
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
+ }
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
- } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
- (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
- // User is at Bottom of Build Log
-
- this.$scrollTopBtn.show();
- this.$scrollBottomBtn.hide();
-
- // Show and Reposition Autoscroll Status Indicator
+ } else {
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollStatusText.addClass('animate');
- } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
- // Build Log height is small
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
- this.$scrollTopBtn.hide();
- this.$scrollBottomBtn.hide();
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
- // Hide Autoscroll Status Indicator
- this.$autoScrollContainer.hide();
- this.$autoScrollStatusText.removeClass('animate');
- }
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
- if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
- this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
- }
- };
-
- Build.prototype.shouldHideSidebarForViewport = function() {
- var bootstrapBreakpoint;
- bootstrapBreakpoint = this.bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
- };
-
- Build.prototype.toggleSidebar = function(shouldHide) {
- var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
- this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
- .toggleClass('sidebar-collapsed', shouldHide);
- this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
- .toggleClass('right-sidebar-collapsed', shouldHide);
- };
-
- Build.prototype.sidebarOnResize = function() {
- this.toggleSidebar(this.shouldHideSidebarForViewport());
- };
-
- Build.prototype.sidebarOnClick = function() {
- if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
- };
-
- Build.prototype.updateArtifactRemoveDate = function() {
- var $date, date;
- $date = $('.js-artifacts-remove');
- if ($date.length) {
- date = $date.text();
- return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
- }
- };
-
- Build.prototype.populateJobs = function(stage) {
- $('.build-job').hide();
- $('.build-job[data-stage="' + stage + '"]').show();
- };
-
- Build.prototype.updateStageDropdownText = function(stage) {
- $('.stage-selection').text(stage);
- };
-
- Build.prototype.updateDropdown = function(e) {
- e.preventDefault();
- var stage = e.currentTarget.text;
- this.updateStageDropdownText(stage);
- this.populateJobs(stage);
- };
-
- Build.prototype.stepTrace = function(e) {
- var $currentTarget;
- e.preventDefault();
- $currentTarget = $(e.currentTarget);
- $.scrollTo($currentTarget.attr('href'), {
- offset: 0
- });
- };
-
- return Build;
- })();
-}).call(window);
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
+ };
+
+ Build.prototype.shouldHideSidebarForViewport = function() {
+ var bootstrapBreakpoint;
+ bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ };
+
+ Build.prototype.toggleSidebar = function(shouldHide) {
+ var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+ this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+ };
+
+ Build.prototype.sidebarOnResize = function() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ };
+
+ Build.prototype.sidebarOnClick = function() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
+ };
+
+ Build.prototype.updateArtifactRemoveDate = function() {
+ var $date, date;
+ $date = $('.js-artifacts-remove');
+ if ($date.length) {
+ date = $date.text();
+ return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ }
+ };
+
+ Build.prototype.populateJobs = function(stage) {
+ $('.build-job').hide();
+ $('.build-job[data-stage="' + stage + '"]').show();
+ };
+
+ Build.prototype.updateStageDropdownText = function(stage) {
+ $('.stage-selection').text(stage);
+ };
+
+ Build.prototype.updateDropdown = function(e) {
+ e.preventDefault();
+ var stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ };
+
+ Build.prototype.stepTrace = function(e) {
+ var $currentTarget;
+ e.preventDefault();
+ $currentTarget = $(e.currentTarget);
+ $.scrollTo($currentTarget.attr('href'), {
+ offset: 0
+ });
+ };
+
+ return Build;
+})();
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index cae9a0ffca4..bd479700fd3 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,26 +1,25 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
-(function() {
- this.BuildArtifacts = (function() {
- function BuildArtifacts() {
- this.disablePropagation();
- this.setupEntryClick();
- }
- BuildArtifacts.prototype.disablePropagation = function() {
- $('.top-block').on('click', '.download', function(e) {
- return e.stopPropagation();
- });
- return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
- return e.stopImmediatePropagation();
- });
- };
+window.BuildArtifacts = (function() {
+ function BuildArtifacts() {
+ this.disablePropagation();
+ this.setupEntryClick();
+ }
- BuildArtifacts.prototype.setupEntryClick = function() {
- return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
- return window.location = this.dataset.link;
- });
- };
+ BuildArtifacts.prototype.disablePropagation = function() {
+ $('.top-block').on('click', '.download', function(e) {
+ return e.stopPropagation();
+ });
+ return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+ return e.stopImmediatePropagation();
+ });
+ };
- return BuildArtifacts;
- })();
-}).call(window);
+ BuildArtifacts.prototype.setupEntryClick = function() {
+ return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
+ return window.location = this.dataset.link;
+ });
+ };
+
+ return BuildArtifacts;
+})();
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js
index 99082b412e2..99082b412e2 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js
diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js
new file mode 100644
index 00000000000..dd4a08a2f31
--- /dev/null
+++ b/app/assets/javascripts/ci_lint_editor.js
@@ -0,0 +1,17 @@
+
+window.gl = window.gl || {};
+
+class CILintEditor {
+ constructor() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.querySelector('#content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ const content = this.editor.getSession().getValue();
+ this.textarea.value = content;
+ });
+ }
+}
+
+gl.CILintEditor = CILintEditor;
diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6
deleted file mode 100644
index 56ffaa765a8..00000000000
--- a/app/assets/javascripts/ci_lint_editor.js.es6
+++ /dev/null
@@ -1,18 +0,0 @@
-(() => {
- window.gl = window.gl || {};
-
- class CILintEditor {
- constructor() {
- this.editor = window.ace.edit('ci-editor');
- this.textarea = document.querySelector('#content');
-
- this.editor.getSession().setMode('ace/mode/yaml');
- this.editor.on('input', () => {
- const content = this.editor.getSession().getValue();
- this.textarea.value = content;
- });
- }
- }
-
- gl.CILintEditor = CILintEditor;
-})();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 566b322eb49..5f637524e30 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,14 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
-(function() {
- this.Commit = (function() {
- function Commit() {
- $('.files .diff-file').each(function() {
- return new CommitFile(this);
- });
- }
+window.Commit = (function() {
+ function Commit() {
+ $('.files .diff-file').each(function() {
+ return new CommitFile(this);
+ });
+ }
- return Commit;
- })();
-}).call(window);
+ return Commit;
+})();
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 49bb64a3472..17d14dc1e79 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -52,6 +52,30 @@
return this.views[viewMode].call(this);
};
+ ImageFile.prototype.initDraggable = function($el, padding, callback) {
+ var dragging = false;
+ var $body = $('body');
+ var $offsetEl = $el.parent();
+
+ $el.off('mousedown').on('mousedown', function() {
+ dragging = true;
+ $body.css('user-select', 'none');
+ });
+
+ $body.off('mouseup').off('mousemove').on('mouseup', function() {
+ dragging = false;
+ $body.css('user-select', '');
+ })
+ .on('mousemove', function(e) {
+ var left;
+ if (!dragging) return;
+
+ left = e.pageX - ($offsetEl.offset().left + padding);
+
+ callback(e, left);
+ });
+ };
+
prepareFrames = function(view) {
var maxHeight, maxWidth;
maxWidth = 0;
@@ -96,26 +120,30 @@
maxHeight = 0;
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
- var ref;
+ var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $('.swipe-frame', view).css({
+ $swipeFrame = $('.swipe-frame', view);
+ $swipeWrap = $('.swipe-wrap', view);
+ $swipeBar = $('.swipe-bar', view);
+
+ $swipeFrame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
- $('.swipe-wrap', view).css({
+ $swipeWrap.css({
width: maxWidth + 1,
height: maxHeight + 2
});
- return $('.swipe-bar', view).css({
+ $swipeBar.css({
left: 0
- }).draggable({
- axis: 'x',
- containment: 'parent',
- drag: function(event) {
- return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
- },
- stop: function(event) {
- return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
+ });
+
+ wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+ if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
+ $swipeWrap.width((maxWidth + 1) - left);
+ $swipeBar.css('left', left);
}
});
};
@@ -128,9 +156,14 @@
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
- var ref;
+ var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $('.onion-skin-frame', view).css({
+ $frame = $('.onion-skin-frame', view);
+ $frameAdded = $('.frame.added', view);
+ $track = $('.drag-track', view);
+ $dragger = $('.dragger', $track);
+
+ $frame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
@@ -138,16 +171,18 @@
width: maxWidth + 1,
height: maxHeight + 2
});
- return $('.dragger', view).css({
+ $dragger.css({
left: dragTrackWidth
- }).draggable({
- axis: 'x',
- containment: 'parent',
- drag: function(event) {
- return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
- },
- stop: function(event) {
- return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
+ });
+
+ framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($dragger, framePadding, function(e, left) {
+ var opacity = left / dragTrackWidth;
+
+ if (opacity >= 0 && opacity <= 1) {
+ $dragger.css('left', left);
+ $frameAdded.css('opacity', opacity);
}
});
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
new file mode 100644
index 00000000000..a9f2d462c31
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -0,0 +1,30 @@
+/* eslint-disable no-param-reassign */
+import CommitPipelinesTable from './pipelines_table';
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+
+/**
+ * Commits View > Pipelines Tab > Pipelines Table.
+ * Merge Request View > Pipelines Tab > Pipelines Table.
+ *
+ * Renders Pipelines table in pipelines tab in the commits show view.
+ * Renders Pipelines table in pipelines tab in the merge request show view.
+ */
+
+$(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ if (gl.commits.PipelinesTableBundle) {
+ gl.commits.PipelinesTableBundle.$destroy(true);
+ }
+
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
+
+ if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
+ gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
+ }
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
deleted file mode 100644
index b5a988df897..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-require('./pipelines_table');
-/**
- * Commits View > Pipelines Tab > Pipelines Table.
- * Merge Request View > Pipelines Tab > Pipelines Table.
- *
- * Renders Pipelines table in pipelines tab in the commits show view.
- * Renders Pipelines table in pipelines tab in the merge request show view.
- */
-
-$(() => {
- window.gl = window.gl || {};
- gl.commits = gl.commits || {};
- gl.commits.pipelines = gl.commits.pipelines || {};
-
- if (gl.commits.PipelinesTableBundle) {
- gl.commits.PipelinesTableBundle.$destroy(true);
- }
-
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
-
- if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
- gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
- }
-});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
deleted file mode 100644
index 8ae98f9bf97..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
- /**
- * FIXME: The url provided to request the pipelines in the new merge request
- * page already has `.json`.
- * This should be fixed when the endpoint is improved.
- *
- * @param {String} root
- */
- constructor(root) {
- let endpoint;
-
- if (root.indexOf('.json') === -1) {
- endpoint = `${root}.json`;
- } else {
- endpoint = root;
- }
- this.pipelines = Vue.resource(endpoint);
- }
-
- /**
- * Given the root param provided when the class is initialized, will
- * make a GET request.
- *
- * @return {Promise}
- */
- all() {
- return this.pipelines.get();
- }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
deleted file mode 100644
index f1b80e45444..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
- constructor() {
- this.state = {};
- this.state.pipelines = [];
- }
-
- storePipelines(pipelines = []) {
- this.state.pipelines = pipelines;
-
- return pipelines;
- }
-
- /**
- * Once the data is received we will start the time ago loops.
- *
- * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
- * update the time to show how long as passed.
- *
- */
- static startTimeAgoLoops() {
- const startTimeLoops = () => {
- this.timeLoopInterval = setInterval(() => {
- this.$children[0].$children.reduce((acc, component) => {
- const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
- acc.push(timeAgoComponent);
- return acc;
- }, []).forEach(e => e.changeTime());
- }, 10000);
- };
-
- startTimeLoops();
-
- const removeIntervals = () => clearInterval(this.timeLoopInterval);
- const startIntervals = () => startTimeLoops();
-
- gl.VueRealtimeListener(removeIntervals, startIntervals);
- }
-}
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
new file mode 100644
index 00000000000..832c4b1bd2a
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -0,0 +1,110 @@
+/* eslint-disable no-new*/
+/* global Flash */
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
+
+/**
+ *
+ * Uses `pipelines-table-component` to render Pipelines table with an API call.
+ * Endpoint is provided in HTML and passed as `endpoint`.
+ * We need a store to store the received environemnts.
+ * We need a service to communicate with the server.
+ *
+ * Necessary SVG in the table are provided as props. This should be refactored
+ * as soon as we have Webpack and can load them directly into JS files.
+ */
+
+export default Vue.component('pipelines-table', {
+ components: {
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const store = new PipelineStore();
+
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
+
+ /**
+ * When the component is about to be mounted, tell the service to fetch the data
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ beforeMount() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ fetchPipelines() {
+ this.isLoading = true;
+ return this.service.getPipelines()
+ .then(response => response.json())
+ .then((json) => {
+ // depending of the endpoint the response can either bring a `pipelines` key or not.
+ const pipelines = json.pipelines || json;
+ this.store.storePipelines(pipelines);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
+ },
+ },
+
+ template: `
+ <div class="pipelines">
+ <div class="realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service" />
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
deleted file mode 100644
index e7c6c063413..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
+++ /dev/null
@@ -1,110 +0,0 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
-
-/**
- *
- * Uses `pipelines-table-component` to render Pipelines table with an API call.
- * Endpoint is provided in HTML and passed as `endpoint`.
- * We need a store to store the received environemnts.
- * We need a service to communicate with the server.
- *
- * Necessary SVG in the table are provided as props. This should be refactored
- * as soon as we have Webpack and can load them directly into JS files.
- */
-
-(() => {
- window.gl = window.gl || {};
- gl.commits = gl.commits || {};
- gl.commits.pipelines = gl.commits.pipelines || {};
-
- gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
-
- components: {
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
- },
-
- /**
- * Accesses the DOM to provide the needed data.
- * Returns the necessary props to render `pipelines-table-component` component.
- *
- * @return {Object}
- */
- data() {
- const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
- const svgsData = document.querySelector('.pipeline-svgs').dataset;
- const store = new PipelineStore();
-
- // Transform svgs DOMStringMap to a plain Object.
- const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
-
- return {
- endpoint: pipelinesTableData.endpoint,
- svgs: svgsObject,
- store,
- state: store.state,
- isLoading: false,
- };
- },
-
- /**
- * When the component is about to be mounted, tell the service to fetch the data
- *
- * A request to fetch the pipelines will be made.
- * In case of a successfull response we will store the data in the provided
- * store, in case of a failed response we need to warn the user.
- *
- */
- beforeMount() {
- const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
-
- this.isLoading = true;
- return pipelinesService.all()
- .then(response => response.json())
- .then((json) => {
- this.store.storePipelines(json);
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
- });
- },
-
- beforeUpdate() {
- if (this.state.pipelines.length && this.$children) {
- PipelineStore.startTimeAgoLoops.call(this, Vue);
- }
- },
-
- template: `
- <div class="pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
-
- <div class="table-holder pipelines"
- v-if="!isLoading && state.pipelines.length > 0">
- <pipelines-table-component
- :pipelines="state.pipelines"
- :svgs="svgs">
- </pipelines-table-component>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ccd895f3bf4..e3f9eaaf39c 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,68 +1,66 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
/* global Pager */
-(function() {
- this.CommitsList = (function() {
- var CommitsList = {};
+window.CommitsList = (function() {
+ var CommitsList = {};
- CommitsList.timer = null;
+ CommitsList.timer = null;
- CommitsList.init = function(limit) {
- $("body").on("click", ".day-commits-table li.commit", function(e) {
- if (e.target.nodeName !== "A") {
- location.href = $(this).attr("url");
- e.stopPropagation();
- return false;
- }
- });
- Pager.init(limit, false, false, function() {
- gl.utils.localTimeAgo($('.js-timeago'));
- });
- this.content = $("#commits-list");
- this.searchField = $("#commits-search");
- this.lastSearch = this.searchField.val();
- return this.initSearch();
- };
+ CommitsList.init = function(limit) {
+ $("body").on("click", ".day-commits-table li.commit", function(e) {
+ if (e.target.nodeName !== "A") {
+ location.href = $(this).attr("url");
+ e.stopPropagation();
+ return false;
+ }
+ });
+ Pager.init(limit, false, false, function() {
+ gl.utils.localTimeAgo($('.js-timeago'));
+ });
+ this.content = $("#commits-list");
+ this.searchField = $("#commits-search");
+ this.lastSearch = this.searchField.val();
+ return this.initSearch();
+ };
- CommitsList.initSearch = function() {
- this.timer = null;
- return this.searchField.keyup((function(_this) {
- return function() {
- clearTimeout(_this.timer);
- return _this.timer = setTimeout(_this.filterResults, 500);
- };
- })(this));
- };
+ CommitsList.initSearch = function() {
+ this.timer = null;
+ return this.searchField.keyup((function(_this) {
+ return function() {
+ clearTimeout(_this.timer);
+ return _this.timer = setTimeout(_this.filterResults, 500);
+ };
+ })(this));
+ };
- CommitsList.filterResults = function() {
- var commitsUrl, form, search;
- form = $(".commits-search-form");
- search = CommitsList.searchField.val();
- if (search === CommitsList.lastSearch) return;
- commitsUrl = form.attr("action") + '?' + form.serialize();
- CommitsList.content.fadeTo('fast', 0.5);
- return $.ajax({
- type: "GET",
- url: form.attr("action"),
- data: form.serialize(),
- complete: function() {
- return CommitsList.content.fadeTo('fast', 1.0);
- },
- success: function(data) {
- CommitsList.lastSearch = search;
- CommitsList.content.html(data.html);
- return history.replaceState({
- page: commitsUrl
- // Change url so if user reload a page - search results are saved
- }, document.title, commitsUrl);
- },
- error: function() {
- CommitsList.lastSearch = null;
- },
- dataType: "json"
- });
- };
+ CommitsList.filterResults = function() {
+ var commitsUrl, form, search;
+ form = $(".commits-search-form");
+ search = CommitsList.searchField.val();
+ if (search === CommitsList.lastSearch) return;
+ commitsUrl = form.attr("action") + '?' + form.serialize();
+ CommitsList.content.fadeTo('fast', 0.5);
+ return $.ajax({
+ type: "GET",
+ url: form.attr("action"),
+ data: form.serialize(),
+ complete: function() {
+ return CommitsList.content.fadeTo('fast', 1.0);
+ },
+ success: function(data) {
+ CommitsList.lastSearch = search;
+ CommitsList.content.html(data.html);
+ return history.replaceState({
+ page: commitsUrl
+ // Change url so if user reload a page - search results are saved
+ }, document.title, commitsUrl);
+ },
+ error: function() {
+ CommitsList.lastSearch = null;
+ },
+ dataType: "json"
+ });
+ };
- return CommitsList;
- })();
-}).call(window);
+ return CommitsList;
+})();
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
new file mode 100644
index 00000000000..36bfe457be9
--- /dev/null
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -0,0 +1,16 @@
+import $ from 'jquery';
+
+// bootstrap jQuery plugins
+import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
+import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
+import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
+import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
+import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
+import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
+import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+
+// custom jQuery functions
+$.fn.extend({
+ disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); },
+ enable() { return $(this).removeAttr('disabled').removeClass('disabled'); },
+});
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
new file mode 100644
index 00000000000..7063f59d446
--- /dev/null
+++ b/app/assets/javascripts/commons/index.js
@@ -0,0 +1,3 @@
+import './polyfills';
+import './jquery';
+import './bootstrap';
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
new file mode 100644
index 00000000000..b53f6284afc
--- /dev/null
+++ b/app/assets/javascripts/commons/jquery.js
@@ -0,0 +1,11 @@
+import 'jquery';
+
+// common jQuery plugins
+import 'jquery-ujs';
+import 'vendor/jquery.endless-scroll';
+import 'vendor/jquery.caret';
+import 'vendor/jquery.atwho';
+import 'vendor/jquery.scrollTo';
+import 'vendor/jquery.nicescroll';
+import 'vendor/jquery.waitforimages';
+import 'select2/select2';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
new file mode 100644
index 00000000000..fbd0db64ca7
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -0,0 +1,10 @@
+// ECMAScript polyfills
+import 'core-js/fn/array/find';
+import 'core-js/fn/object/assign';
+import 'core-js/fn/promise';
+import 'core-js/fn/string/code-point-at';
+import 'core-js/fn/string/from-code-point';
+
+// Browser polyfills
+import './polyfills/custom_event';
+import './polyfills/element';
diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js
new file mode 100644
index 00000000000..aea61b82d03
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/custom_event.js
@@ -0,0 +1,9 @@
+if (typeof window.CustomEvent !== 'function') {
+ window.CustomEvent = function CustomEvent(event, params) {
+ const evt = document.createEvent('CustomEvent');
+ const evtParams = params || { bubbles: false, cancelable: false, detail: undefined };
+ evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
+ return evt;
+ };
+ window.CustomEvent.prototype = Event;
+}
diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js
new file mode 100644
index 00000000000..9a1f73bf2ac
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/element.js
@@ -0,0 +1,20 @@
+Element.prototype.closest = Element.prototype.closest ||
+ function closest(selector, selectedElement = this) {
+ if (!selectedElement) return null;
+ return selectedElement.matches(selector) ?
+ selectedElement :
+ Element.prototype.closest(selector, selectedElement.parentElement);
+ };
+
+Element.prototype.matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector ||
+ function matches(selector) {
+ const elms = (this.document || this.ownerDocument).querySelectorAll(selector);
+ let i = elms.length - 1;
+ while (i >= 0 && elms.item(i) !== this) { i -= 1; }
+ return i > -1;
+ };
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 15df105d4cc..9e5dbd64a7e 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,91 +1,90 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-(function() {
- this.Compare = (function() {
- function Compare(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('field-name'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
+
+window.Compare = (function() {
+ function Compare(opts) {
+ this.opts = opts;
+ this.source_loading = $(".js-source-loading");
+ this.target_loading = $(".js-target-loading");
+ $('.js-compare-dropdown').each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return $dropdown.glDropdown({
+ selectable: true,
+ fieldName: $dropdown.data('field-name'),
+ filterable: true,
+ id: function(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(e, el) {
+ if ($dropdown.is('.js-target-branch')) {
+ return _this.getTargetHtml();
+ } else if ($dropdown.is('.js-source-branch')) {
+ return _this.getSourceHtml();
+ } else if ($dropdown.is('.js-target-project')) {
+ return _this.getTargetProject();
}
- });
- };
- })(this));
- this.initialState();
- }
+ }
+ });
+ };
+ })(this));
+ this.initialState();
+ }
- Compare.prototype.initialState = function() {
- this.getSourceHtml();
- return this.getTargetHtml();
- };
+ Compare.prototype.initialState = function() {
+ this.getSourceHtml();
+ return this.getTargetHtml();
+ };
- Compare.prototype.getTargetProject = function() {
- return $.ajax({
- url: this.opts.targetProjectUrl,
- data: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val()
- },
- beforeSend: function() {
- return $('.mr_target_commit').empty();
- },
- success: function(html) {
- return $('.js-target-branch-dropdown .dropdown-content').html(html);
- }
- });
- };
+ Compare.prototype.getTargetProject = function() {
+ return $.ajax({
+ url: this.opts.targetProjectUrl,
+ data: {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val()
+ },
+ beforeSend: function() {
+ return $('.mr_target_commit').empty();
+ },
+ success: function(html) {
+ return $('.js-target-branch-dropdown .dropdown-content').html(html);
+ }
+ });
+ };
- Compare.prototype.getSourceHtml = function() {
- return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- };
+ Compare.prototype.getSourceHtml = function() {
+ return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
+ ref: $("input[name='merge_request[source_branch]']").val()
+ });
+ };
- Compare.prototype.getTargetHtml = function() {
- return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- };
+ Compare.prototype.getTargetHtml = function() {
+ return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val(),
+ ref: $("input[name='merge_request[target_branch]']").val()
+ });
+ };
- Compare.prototype.sendAjax = function(url, loading, target, data) {
- var $target;
- $target = $(target);
- return $.ajax({
- url: url,
- data: data,
- beforeSend: function() {
- loading.show();
- return $target.empty();
- },
- success: function(html) {
- loading.hide();
- $target.html(html);
- var className = '.' + $target[0].className.replace(' ', '.');
- gl.utils.localTimeAgo($('.js-timeago', className));
- }
- });
- };
+ Compare.prototype.sendAjax = function(url, loading, target, data) {
+ var $target;
+ $target = $(target);
+ return $.ajax({
+ url: url,
+ data: data,
+ beforeSend: function() {
+ loading.show();
+ return $target.empty();
+ },
+ success: function(html) {
+ loading.hide();
+ $target.html(html);
+ var className = '.' + $target[0].className.replace(' ', '.');
+ gl.utils.localTimeAgo($('.js-timeago', className));
+ }
+ });
+ };
- return Compare;
- })();
-}).call(window);
+ return Compare;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
new file mode 100644
index 00000000000..72c0d98d47c
--- /dev/null
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -0,0 +1,68 @@
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
+
+window.CompareAutocomplete = (function() {
+ function CompareAutocomplete() {
+ this.initDropdown();
+ }
+
+ CompareAutocomplete.prototype.initDropdown = function() {
+ return $('.js-compare-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ const $dropdownContainer = $dropdown.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+ $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ }
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ fieldName: $dropdown.data('field-name'),
+ filterInput: 'input[type="search"]',
+ renderRow: function(ref) {
+ var link;
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ } else {
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ return $('<li />').append(link);
+ }
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ }
+ });
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+ const text = $filterInput.val();
+ $fieldInput.val(text);
+ $('.dropdown-toggle-text', $dropdown).text(text);
+ $dropdownContainer.removeClass('open');
+ });
+
+ $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+ if ($dropdown.hasClass('has-tooltip')) {
+ $dropdown.tooltip('fixTitle');
+ }
+ });
+ });
+ };
+
+ return CompareAutocomplete;
+})();
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
deleted file mode 100644
index 1eca973e069..00000000000
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ /dev/null
@@ -1,69 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
-
-(function() {
- this.CompareAutocomplete = (function() {
- function CompareAutocomplete() {
- this.initDropdown();
- }
-
- CompareAutocomplete.prototype.initDropdown = function() {
- return $('.js-compare-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- const $dropdownContainer = $dropdown.closest('.dropdown');
- const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
- $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref')
- }
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterByText: true,
- fieldName: $dropdown.data('field-name'),
- filterInput: 'input[type="search"]',
- renderRow: function(ref) {
- var link;
- if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
- } else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
- return $('<li />').append(link);
- }
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- }
- });
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
- const text = $filterInput.val();
- $fieldInput.val(text);
- $('.dropdown-toggle-text', $dropdown).text(text);
- $dropdownContainer.removeClass('open');
- });
-
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
- $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
- if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
- }
- });
- });
- };
-
- return CompareAutocomplete;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index a1c1b721228..b375b61202e 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,31 +1,30 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
-(function() {
- this.ConfirmDangerModal = (function() {
- function ConfirmDangerModal(form, text) {
- var project_path, submit;
- this.form = form;
- $('.js-confirm-text').text(text || '');
- $('.js-confirm-danger-input').val('');
- $('#modal-confirm-danger').modal('show');
- project_path = $('.js-confirm-danger-match').text();
- submit = $('.js-confirm-danger-submit');
- submit.disable();
- $('.js-confirm-danger-input').off('input');
- $('.js-confirm-danger-input').on('input', function() {
- if (gl.utils.rstrip($(this).val()) === project_path) {
- return submit.enable();
- } else {
- return submit.disable();
- }
- });
- $('.js-confirm-danger-submit').off('click');
- $('.js-confirm-danger-submit').on('click', (function(_this) {
- return function() {
- return _this.form.submit();
- };
- })(this));
- }
- return ConfirmDangerModal;
- })();
-}).call(window);
+window.ConfirmDangerModal = (function() {
+ function ConfirmDangerModal(form, text) {
+ var project_path, submit;
+ this.form = form;
+ $('.js-confirm-text').text(text || '');
+ $('.js-confirm-danger-input').val('');
+ $('#modal-confirm-danger').modal('show');
+ project_path = $('.js-confirm-danger-match').text();
+ submit = $('.js-confirm-danger-submit');
+ submit.disable();
+ $('.js-confirm-danger-input').off('input');
+ $('.js-confirm-danger-input').on('input', function() {
+ if (gl.utils.rstrip($(this).val()) === project_path) {
+ return submit.enable();
+ } else {
+ return submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit').off('click');
+ $('.js-confirm-danger-submit').on('click', (function(_this) {
+ return function() {
+ return _this.form.submit();
+ };
+ })(this));
+ }
+
+ return ConfirmDangerModal;
+})();
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
new file mode 100644
index 00000000000..570799c030e
--- /dev/null
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -0,0 +1,402 @@
+/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+
+require('./lib/utils/common_utils');
+
+const gfmRules = {
+ // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
+ // GitLab Flavored Markdown (GFM) to HTML.
+ // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
+ // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+ // from GFM should have a handler here, in reverse order.
+ // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+ InlineDiffFilter: {
+ 'span.idiff.addition'(el, text) {
+ return `{+${text}+}`;
+ },
+ 'span.idiff.deletion'(el, text) {
+ return `{-${text}-}`;
+ },
+ },
+ TaskListFilter: {
+ 'input[type=checkbox].task-list-item-checkbox'(el, text) {
+ return `[${el.checked ? 'x' : ' '}]`;
+ },
+ },
+ ReferenceFilter: {
+ '.tooltip'(el, text) {
+ return '';
+ },
+ 'a.gfm:not([data-link=true])'(el, text) {
+ return el.dataset.original || text;
+ },
+ },
+ AutolinkFilter: {
+ 'a'(el, text) {
+ // Fallback on the regular MarkdownFilter's `a` handler.
+ if (text !== el.getAttribute('href')) return false;
+
+ return text;
+ },
+ },
+ TableOfContentsFilter: {
+ 'ul.section-nav'(el, text) {
+ return '[[_TOC_]]';
+ },
+ },
+ EmojiFilter: {
+ 'img.emoji'(el, text) {
+ return el.getAttribute('alt');
+ },
+ 'gl-emoji'(el, text) {
+ return `:${el.getAttribute('data-name')}:`;
+ },
+ },
+ ImageLinkFilter: {
+ 'a.no-attachment-icon'(el, text) {
+ return text;
+ },
+ },
+ VideoLinkFilter: {
+ '.video-container'(el, text) {
+ const videoEl = el.querySelector('video');
+ if (!videoEl) return false;
+
+ return CopyAsGFM.nodeToGFM(videoEl);
+ },
+ 'video'(el, text) {
+ return `![${el.dataset.title}](${el.getAttribute('src')})`;
+ },
+ },
+ MathFilter: {
+ 'pre.code.math[data-math-style=display]'(el, text) {
+ return `\`\`\`math\n${text.trim()}\n\`\`\``;
+ },
+ 'code.code.math[data-math-style=inline]'(el, text) {
+ return `$\`${text}\`$`;
+ },
+ 'span.katex-display span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
+ },
+ 'span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
+ },
+ 'span.katex-html'(el, text) {
+ // We don't want to include the content of this element in the copied text.
+ return '';
+ },
+ 'annotation[encoding="application/x-tex"]'(el, text) {
+ return text.trim();
+ },
+ },
+ SanitizationFilter: {
+ 'a[name]:not([href]):empty'(el, text) {
+ return el.outerHTML;
+ },
+ 'dl'(el, text) {
+ let lines = text.trim().split('\n');
+ // Add two spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ lines = lines.map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ });
+
+ return `<dl>\n${lines.join('\n')}\n</dl>`;
+ },
+ 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>`;
+ },
+ },
+ SyntaxHighlightFilter: {
+ 'pre.code.highlight'(el, t) {
+ const text = t.trimRight();
+
+ let lang = el.getAttribute('lang');
+ if (!lang || lang === 'plaintext') {
+ lang = '';
+ }
+
+ // Prefixes lines with 4 spaces if the code contains triple backticks
+ if (lang === '' && text.match(/^```/gm)) {
+ return text.split('\n').map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ }).join('\n');
+ }
+
+ return `\`\`\`${lang}\n${text}\n\`\`\``;
+ },
+ 'pre > code'(el, text) {
+ // Don't wrap code blocks in ``
+ return text;
+ },
+ },
+ MarkdownFilter: {
+ 'br'(el, text) {
+ // Two spaces at the end of a line are turned into a BR
+ return ' ';
+ },
+ 'code'(el, text) {
+ let backtickCount = 1;
+ const backtickMatch = text.match(/`+/);
+ if (backtickMatch) {
+ backtickCount = backtickMatch[0].length + 1;
+ }
+
+ const backticks = Array(backtickCount + 1).join('`');
+ const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
+
+ return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
+ },
+ 'blockquote'(el, text) {
+ return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
+ },
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ 'a.anchor'(el, text) {
+ // Don't render a Markdown link for the anchor link inside a heading
+ return text;
+ },
+ 'a'(el, text) {
+ return `[${text}](${el.getAttribute('href')})`;
+ },
+ 'li'(el, text) {
+ const lines = text.trim().split('\n');
+ const firstLine = `- ${lines.shift()}`;
+ // Add four spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ const nextLines = lines.map((s) => {
+ if (s.trim().length === 0) return '';
+
+ return ` ${s}`;
+ });
+
+ return `${firstLine}\n${nextLines.join('\n')}`;
+ },
+ 'ul'(el, text) {
+ return text;
+ },
+ 'ol'(el, text) {
+ // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
+ return text.replace(/^- /mg, '1. ');
+ },
+ 'h1'(el, text) {
+ return `# ${text.trim()}`;
+ },
+ 'h2'(el, text) {
+ return `## ${text.trim()}`;
+ },
+ 'h3'(el, text) {
+ return `### ${text.trim()}`;
+ },
+ 'h4'(el, text) {
+ return `#### ${text.trim()}`;
+ },
+ 'h5'(el, text) {
+ return `##### ${text.trim()}`;
+ },
+ 'h6'(el, text) {
+ return `###### ${text.trim()}`;
+ },
+ 'strong'(el, text) {
+ return `**${text}**`;
+ },
+ 'em'(el, text) {
+ return `_${text}_`;
+ },
+ 'del'(el, text) {
+ return `~~${text}~~`;
+ },
+ 'sup'(el, text) {
+ return `^${text}`;
+ },
+ 'hr'(el, text) {
+ return '-----';
+ },
+ 'table'(el, text) {
+ const theadEl = el.querySelector('thead');
+ const tbodyEl = el.querySelector('tbody');
+ if (!theadEl || !tbodyEl) return false;
+
+ const theadText = CopyAsGFM.nodeToGFM(theadEl);
+ const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
+
+ return theadText + tbodyText;
+ },
+ 'thead'(el, text) {
+ const cells = _.map(el.querySelectorAll('th'), (cell) => {
+ let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
+
+ let before = '';
+ let after = '';
+ switch (cell.style.textAlign) {
+ case 'center':
+ before = ':';
+ after = ':';
+ chars -= 2;
+ break;
+ case 'right':
+ after = ':';
+ chars -= 1;
+ break;
+ default:
+ break;
+ }
+
+ chars = Math.max(chars, 3);
+
+ const middle = Array(chars + 1).join('-');
+
+ return before + middle + after;
+ });
+
+ return `${text}|${cells.join('|')}|`;
+ },
+ 'tr'(el, text) {
+ const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
+ return `| ${cells.join(' | ')} |`;
+ },
+ },
+};
+
+class CopyAsGFM {
+ constructor() {
+ $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
+ $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
+ $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this));
+ }
+
+ copyAsGFM(e, transformer) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!documentFragment) return;
+
+ const el = transformer(documentFragment.cloneNode(true));
+ if (!el) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ clipboardData.setData('text/plain', el.textContent);
+ clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
+ }
+
+ pasteGFM(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const gfm = clipboardData.getData('text/x-gfm');
+ if (!gfm) return;
+
+ e.preventDefault();
+
+ window.gl.utils.insertText(e.target, gfm);
+ }
+
+ static transformGFMSelection(documentFragment) {
+ // If the documentFragment contains more than just Markdown, don't copy as GFM.
+ if (documentFragment.querySelector('.md, .wiki')) return null;
+
+ return documentFragment;
+ }
+
+ static transformCodeSelection(documentFragment) {
+ const lineEls = documentFragment.querySelectorAll('.line');
+
+ let codeEl;
+ if (lineEls.length > 1) {
+ codeEl = document.createElement('pre');
+ codeEl.className = 'code highlight';
+
+ const lang = lineEls[0].getAttribute('lang');
+ if (lang) {
+ codeEl.setAttribute('lang', lang);
+ }
+ } else {
+ codeEl = document.createElement('code');
+ }
+
+ if (lineEls.length > 0) {
+ for (let i = 0; i < lineEls.length; i += 1) {
+ const lineEl = lineEls[i];
+ codeEl.appendChild(lineEl);
+ codeEl.appendChild(document.createTextNode('\n'));
+ }
+ } else {
+ codeEl.appendChild(documentFragment);
+ }
+
+ return codeEl;
+ }
+
+ static nodeToGFM(node) {
+ if (node.nodeType === Node.COMMENT_NODE) {
+ return '';
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent;
+ }
+
+ const text = this.innerGFM(node);
+
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ return text;
+ }
+
+ for (const filter in gfmRules) {
+ const rules = gfmRules[filter];
+
+ for (const selector in rules) {
+ const func = rules[selector];
+
+ if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
+
+ const result = func(node, text);
+ if (result === false) continue;
+
+ return result;
+ }
+ }
+
+ return text;
+ }
+
+ static innerGFM(parentNode) {
+ const nodes = parentNode.childNodes;
+
+ const clonedParentNode = parentNode.cloneNode(true);
+ const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+
+ for (let i = 0; i < nodes.length; i += 1) {
+ const node = nodes[i];
+ const clonedNode = clonedNodes[i];
+
+ const text = this.nodeToGFM(node);
+
+ // `clonedNode.replaceWith(text)` is not yet widely supported
+ clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
+ }
+
+ return clonedParentNode.innerText || clonedParentNode.textContent;
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.CopyAsGFM = CopyAsGFM;
+
+new CopyAsGFM();
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
deleted file mode 100644
index 4bd537a6f28..00000000000
--- a/app/assets/javascripts/copy_as_gfm.js.es6
+++ /dev/null
@@ -1,358 +0,0 @@
-/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
-/* jshint esversion: 6 */
-
-require('./lib/utils/common_utils');
-
-(() => {
- const gfmRules = {
- // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
- // GitLab Flavored Markdown (GFM) to HTML.
- // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
- // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
- // from GFM should have a handler here, in reverse order.
- // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
- InlineDiffFilter: {
- 'span.idiff.addition'(el, text) {
- return `{+${text}+}`;
- },
- 'span.idiff.deletion'(el, text) {
- return `{-${text}-}`;
- },
- },
- TaskListFilter: {
- 'input[type=checkbox].task-list-item-checkbox'(el, text) {
- return `[${el.checked ? 'x' : ' '}]`;
- },
- },
- ReferenceFilter: {
- 'a.gfm:not([data-link=true])'(el, text) {
- return el.dataset.original || text;
- },
- },
- AutolinkFilter: {
- 'a'(el, text) {
- // Fallback on the regular MarkdownFilter's `a` handler.
- if (text !== el.getAttribute('href')) return false;
-
- return text;
- },
- },
- TableOfContentsFilter: {
- 'ul.section-nav'(el, text) {
- return '[[_TOC_]]';
- },
- },
- EmojiFilter: {
- 'img.emoji'(el, text) {
- return el.getAttribute('alt');
- },
- },
- ImageLinkFilter: {
- 'a.no-attachment-icon'(el, text) {
- return text;
- },
- },
- VideoLinkFilter: {
- '.video-container'(el, text) {
- const videoEl = el.querySelector('video');
- if (!videoEl) return false;
-
- return CopyAsGFM.nodeToGFM(videoEl);
- },
- 'video'(el, text) {
- return `![${el.dataset.title}](${el.getAttribute('src')})`;
- },
- },
- MathFilter: {
- 'pre.code.math[data-math-style=display]'(el, text) {
- return `\`\`\`math\n${text.trim()}\n\`\`\``;
- },
- 'code.code.math[data-math-style=inline]'(el, text) {
- return `$\`${text}\`$`;
- },
- 'span.katex-display span.katex-mathml'(el, text) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
- },
- 'span.katex-mathml'(el, text) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
- },
- 'span.katex-html'(el, text) {
- // We don't want to include the content of this element in the copied text.
- return '';
- },
- 'annotation[encoding="application/x-tex"]'(el, text) {
- return text.trim();
- },
- },
- SanitizationFilter: {
- 'a[name]:not([href]):empty'(el, text) {
- return el.outerHTML;
- },
- 'dl'(el, text) {
- let lines = text.trim().split('\n');
- // Add two spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- lines = lines.map((l) => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- });
-
- return `<dl>\n${lines.join('\n')}\n</dl>`;
- },
- 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>`;
- },
- },
- SyntaxHighlightFilter: {
- 'pre.code.highlight'(el, t) {
- const text = t.trim();
-
- let lang = el.getAttribute('lang');
- if (lang === 'plaintext') {
- lang = '';
- }
-
- // Prefixes lines with 4 spaces if the code contains triple backticks
- if (lang === '' && text.match(/^```/gm)) {
- return text.split('\n').map((l) => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- }).join('\n');
- }
-
- return `\`\`\`${lang}\n${text}\n\`\`\``;
- },
- 'pre > code'(el, text) {
- // Don't wrap code blocks in ``
- return text;
- },
- },
- MarkdownFilter: {
- 'br'(el, text) {
- // Two spaces at the end of a line are turned into a BR
- return ' ';
- },
- 'code'(el, text) {
- let backtickCount = 1;
- const backtickMatch = text.match(/`+/);
- if (backtickMatch) {
- backtickCount = backtickMatch[0].length + 1;
- }
-
- const backticks = Array(backtickCount + 1).join('`');
- const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
-
- return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
- },
- 'blockquote'(el, text) {
- return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
- },
- 'img'(el, text) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
- },
- 'a.anchor'(el, text) {
- // Don't render a Markdown link for the anchor link inside a heading
- return text;
- },
- 'a'(el, text) {
- return `[${text}](${el.getAttribute('href')})`;
- },
- 'li'(el, text) {
- const lines = text.trim().split('\n');
- const firstLine = `- ${lines.shift()}`;
- // Add four spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- const nextLines = lines.map((s) => {
- if (s.trim().length === 0) return '';
-
- return ` ${s}`;
- });
-
- return `${firstLine}\n${nextLines.join('\n')}`;
- },
- 'ul'(el, text) {
- return text;
- },
- 'ol'(el, text) {
- // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
- return text.replace(/^- /mg, '1. ');
- },
- 'h1'(el, text) {
- return `# ${text.trim()}`;
- },
- 'h2'(el, text) {
- return `## ${text.trim()}`;
- },
- 'h3'(el, text) {
- return `### ${text.trim()}`;
- },
- 'h4'(el, text) {
- return `#### ${text.trim()}`;
- },
- 'h5'(el, text) {
- return `##### ${text.trim()}`;
- },
- 'h6'(el, text) {
- return `###### ${text.trim()}`;
- },
- 'strong'(el, text) {
- return `**${text}**`;
- },
- 'em'(el, text) {
- return `_${text}_`;
- },
- 'del'(el, text) {
- return `~~${text}~~`;
- },
- 'sup'(el, text) {
- return `^${text}`;
- },
- 'hr'(el, text) {
- return '-----';
- },
- 'table'(el, text) {
- const theadEl = el.querySelector('thead');
- const tbodyEl = el.querySelector('tbody');
- if (!theadEl || !tbodyEl) return false;
-
- const theadText = CopyAsGFM.nodeToGFM(theadEl);
- const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
-
- return theadText + tbodyText;
- },
- 'thead'(el, text) {
- const cells = _.map(el.querySelectorAll('th'), (cell) => {
- let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
-
- let before = '';
- let after = '';
- switch (cell.style.textAlign) {
- case 'center':
- before = ':';
- after = ':';
- chars -= 2;
- break;
- case 'right':
- after = ':';
- chars -= 1;
- break;
- default:
- break;
- }
-
- chars = Math.max(chars, 3);
-
- const middle = Array(chars + 1).join('-');
-
- return before + middle + after;
- });
-
- return `${text}|${cells.join('|')}|`;
- },
- 'tr'(el, text) {
- const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
- return `| ${cells.join(' | ')} |`;
- },
- },
- };
-
- class CopyAsGFM {
- constructor() {
- $(document).on('copy', '.md, .wiki', this.handleCopy);
- $(document).on('paste', '.js-gfm-input', this.handlePaste);
- }
-
- handleCopy(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const documentFragment = window.gl.utils.getSelectedFragment();
- if (!documentFragment) return;
-
- // If the documentFragment contains more than just Markdown, don't copy as GFM.
- if (documentFragment.querySelector('.md, .wiki')) return;
-
- e.preventDefault();
- clipboardData.setData('text/plain', documentFragment.textContent);
-
- const gfm = CopyAsGFM.nodeToGFM(documentFragment);
- clipboardData.setData('text/x-gfm', gfm);
- }
-
- handlePaste(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const gfm = clipboardData.getData('text/x-gfm');
- if (!gfm) return;
-
- e.preventDefault();
-
- window.gl.utils.insertText(e.target, gfm);
- }
-
- static nodeToGFM(node) {
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent;
- }
-
- const text = this.innerGFM(node);
-
- if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
- return text;
- }
-
- for (const filter in gfmRules) {
- const rules = gfmRules[filter];
-
- for (const selector in rules) {
- const func = rules[selector];
-
- if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
-
- const result = func(node, text);
- if (result === false) continue;
-
- return result;
- }
- }
-
- return text;
- }
-
- static innerGFM(parentNode) {
- const nodes = parentNode.childNodes;
-
- const clonedParentNode = parentNode.cloneNode(true);
- const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
-
- for (let i = 0; i < nodes.length; i += 1) {
- const node = nodes[i];
- const clonedNode = clonedNodes[i];
-
- const text = this.nodeToGFM(node);
-
- // `clonedNode.replaceWith(text)` is not yet widely supported
- clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
- }
-
- return clonedParentNode.innerText || clonedParentNode.textContent;
- }
- }
-
- window.gl = window.gl || {};
- window.gl.CopyAsGFM = CopyAsGFM;
-
- new CopyAsGFM();
-})();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 615f485e18a..6dbec50b890 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,49 +1,46 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
-/* global Clipboard */
-
-window.Clipboard = require('vendor/clipboard');
-
-(function() {
- var genericError, genericSuccess, showTooltip;
-
- genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied');
- // Clear the selection and blur the trigger so it loses its border
- e.clearSelection();
- return $(e.trigger).blur();
- };
-
- // Safari doesn't support `execCommand`, so instead we inform the user to
- // copy manually.
- //
- // See http://clipboardjs.com/#browser-support
- genericError = function(e) {
- var key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
- }
- return showTooltip(e.trigger, "Press " + key + "-C to copy");
- };
-
- showTooltip = function(target, title) {
- var $target = $(target);
- var originalTitle = $target.data('original-title');
-
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
- };
-
- $(function() {
- var clipboard;
-
- clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
- clipboard.on('success', genericSuccess);
- return clipboard.on('error', genericError);
- });
-}).call(window);
+
+import Clipboard from 'vendor/clipboard';
+
+var genericError, genericSuccess, showTooltip;
+
+genericSuccess = function(e) {
+ showTooltip(e.trigger, 'Copied');
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ return $(e.trigger).blur();
+};
+
+// Safari doesn't support `execCommand`, so instead we inform the user to
+// copy manually.
+//
+// See http://clipboardjs.com/#browser-support
+genericError = function(e) {
+ var key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;'; // Command
+ } else {
+ key = 'Ctrl';
+ }
+ return showTooltip(e.trigger, "Press " + key + "-C to copy");
+};
+
+showTooltip = function(target, title) {
+ var $target = $(target);
+ var originalTitle = $target.data('original-title');
+
+ $target
+ .attr('title', 'Copied')
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+};
+
+$(function() {
+ var clipboard;
+
+ clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ return clipboard.on('error', genericError);
+});
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
new file mode 100644
index 00000000000..121d64db789
--- /dev/null
+++ b/app/assets/javascripts/create_label.js
@@ -0,0 +1,127 @@
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* global Api */
+
+class CreateLabelDropdown {
+ constructor ($el, namespacePath, projectPath) {
+ this.$el = $el;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
+ this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+ this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+ this.$newLabelField = $('#new_label_name', this.$el);
+ this.$newColorField = $('#new_label_color', this.$el);
+ this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+ this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.disable();
+
+ this.cleanBinding();
+ this.addBinding();
+ }
+
+ cleanBinding () {
+ this.$colorSuggestions.off('click');
+ this.$newLabelField.off('keyup change');
+ this.$newColorField.off('keyup change');
+ this.$dropdownBack.off('click');
+ this.$cancelButton.off('click');
+ this.$newLabelCreateButton.off('click');
+ }
+
+ addBinding () {
+ const self = this;
+
+ this.$colorSuggestions.on('click', function (e) {
+ const $this = $(this);
+ self.addColorValue(e, $this);
+ });
+
+ this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+
+ this.$dropdownBack.on('click', this.resetForm.bind(this));
+
+ this.$cancelButton.on('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ self.resetForm();
+ self.$dropdownBack.trigger('click');
+ });
+
+ this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+ }
+
+ addColorValue (e, $this) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$newColorField.val($this.data('color')).trigger('change');
+ this.$colorPreview
+ .css('background-color', $this.data('color'))
+ .parent()
+ .addClass('is-active');
+ }
+
+ enableLabelCreateButton () {
+ if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.enable();
+ } else {
+ this.$newLabelCreateButton.disable();
+ }
+ }
+
+ resetForm () {
+ this.$newLabelField
+ .val('')
+ .trigger('change');
+
+ this.$newColorField
+ .val('')
+ .trigger('change');
+
+ this.$colorPreview
+ .css('background-color', '')
+ .parent()
+ .removeClass('is-active');
+ }
+
+ saveLabel (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
+ color: this.$newColorField.val()
+ }, (label) => {
+ this.$newLabelCreateButton.enable();
+
+ if (label.message) {
+ let errors;
+
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = Object.keys(label.message).map(key =>
+ `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
+ ).join("<br/>");
+ }
+
+ this.$newLabelError
+ .html(errors)
+ .show();
+ } else {
+ this.$dropdownBack.trigger('click');
+
+ $(document).trigger('created.label', label);
+ }
+ });
+ }
+}
+
+window.gl = window.gl || {};
+gl.CreateLabelDropdown = CreateLabelDropdown;
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
deleted file mode 100644
index 85384d98126..00000000000
--- a/app/assets/javascripts/create_label.js.es6
+++ /dev/null
@@ -1,132 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
-/* global Api */
-
-(function (w) {
- class CreateLabelDropdown {
- constructor ($el, namespacePath, projectPath) {
- this.$el = $el;
- this.namespacePath = namespacePath;
- this.projectPath = projectPath;
- this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
- this.$cancelButton = $('.js-cancel-label-btn', this.$el);
- this.$newLabelField = $('#new_label_name', this.$el);
- this.$newColorField = $('#new_label_color', this.$el);
- this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
- this.$newLabelError = $('.js-label-error', this.$el);
- this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
- this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
-
- this.$newLabelError.hide();
- this.$newLabelCreateButton.disable();
-
- this.cleanBinding();
- this.addBinding();
- }
-
- cleanBinding () {
- this.$colorSuggestions.off('click');
- this.$newLabelField.off('keyup change');
- this.$newColorField.off('keyup change');
- this.$dropdownBack.off('click');
- this.$cancelButton.off('click');
- this.$newLabelCreateButton.off('click');
- }
-
- addBinding () {
- const self = this;
-
- this.$colorSuggestions.on('click', function (e) {
- const $this = $(this);
- self.addColorValue(e, $this);
- });
-
- this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
- this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
-
- this.$dropdownBack.on('click', this.resetForm.bind(this));
-
- this.$cancelButton.on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- self.resetForm();
- self.$dropdownBack.trigger('click');
- });
-
- this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
- }
-
- addColorValue (e, $this) {
- e.preventDefault();
- e.stopPropagation();
-
- this.$newColorField.val($this.data('color')).trigger('change');
- this.$colorPreview
- .css('background-color', $this.data('color'))
- .parent()
- .addClass('is-active');
- }
-
- enableLabelCreateButton () {
- if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
- this.$newLabelError.hide();
- this.$newLabelCreateButton.enable();
- } else {
- this.$newLabelCreateButton.disable();
- }
- }
-
- resetForm () {
- this.$newLabelField
- .val('')
- .trigger('change');
-
- this.$newColorField
- .val('')
- .trigger('change');
-
- this.$colorPreview
- .css('background-color', '')
- .parent()
- .removeClass('is-active');
- }
-
- saveLabel (e) {
- e.preventDefault();
- e.stopPropagation();
-
- Api.newLabel(this.namespacePath, this.projectPath, {
- title: this.$newLabelField.val(),
- color: this.$newColorField.val()
- }, (label) => {
- this.$newLabelCreateButton.enable();
-
- if (label.message) {
- let errors;
-
- if (typeof label.message === 'string') {
- errors = label.message;
- } else {
- errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
- ).join("<br/>");
- }
-
- this.$newLabelError
- .html(errors)
- .show();
- } else {
- this.$dropdownBack.trigger('click');
-
- $(document).trigger('created.label', label);
- }
- });
- }
- }
-
- if (!w.gl) {
- w.gl = {};
- }
-
- gl.CreateLabelDropdown = CreateLabelDropdown;
-})(window);
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index b83a4c63fad..b83a4c63fad 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index cb1687dcc7a..cb1687dcc7a 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
new file mode 100644
index 00000000000..42e1bbce744
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -0,0 +1,56 @@
+/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import iconCommit from '../svg/icon_commit.svg';
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StagePlanComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+
+ data() {
+ return { iconCommit };
+ },
+
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ <span v-if="items.length === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="commit in items" class="stage-event-item">
+ <div class="item-details item-conmmit-component">
+ <img class="avatar" :src="commit.author.avatarUrl">
+ <h5 class="item-title commit-title">
+ <a :href="commit.commitUrl">
+ {{ commit.title }}
+ </a>
+ </h5>
+ <span>
+ First
+ <span class="commit-icon">${iconCommit}</span>
+ <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
+ pushed by
+ <a :href="commit.author.webUrl" class="commit-author-link">
+ {{ commit.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="commit.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
deleted file mode 100644
index 8652479e7bf..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
+++ /dev/null
@@ -1,50 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
-
- global.cycleAnalytics.StagePlanComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- <span v-if="items.length === 50" class="events-info pull-right">
- <i class="fa fa-warning has-tooltip"
- title="Limited to showing 50 events at most"
- data-placement="top"></i>
- Showing 50 events
- </span>
- </div>
- <ul class="stage-event-list">
- <li v-for="commit in items" class="stage-event-item">
- <div class="item-details item-conmmit-component">
- <img class="avatar" :src="commit.author.avatarUrl">
- <h5 class="item-title commit-title">
- <a :href="commit.commitUrl">
- {{ commit.title }}
- </a>
- </h5>
- <span>
- First
- <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
- <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
- pushed by
- <a :href="commit.author.webUrl" class="commit-author-link">
- {{ commit.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="commit.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 73f4205b578..73f4205b578 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 501ffb1fac9..501ffb1fac9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
new file mode 100644
index 00000000000..8fa63734cf1
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -0,0 +1,48 @@
+/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import iconBranch from '../svg/icon_branch.svg';
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageStagingComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ data() {
+ return { iconBranch };
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <img class="avatar" :src="build.author.avatarUrl">
+ <h5 class="item-title">
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="build-date">{{ build.date }}</a>
+ by
+ <a :href="build.author.webUrl" class="issue-author-link">
+ {{ build.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
deleted file mode 100644
index 82622232f64..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
+++ /dev/null
@@ -1,44 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
-
- global.cycleAnalytics.StageStagingComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <img class="avatar" :src="build.author.avatarUrl">
- <h5 class="item-title">
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
- <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="build-date">{{ build.date }}</a>
- by
- <a :href="build.author.webUrl" class="issue-author-link">
- {{ build.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
new file mode 100644
index 00000000000..0015249cfaa
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -0,0 +1,49 @@
+/* eslint-disable no-param-reassign */
+import Vue from 'vue';
+import iconBuildStatus from '../svg/icon_build_status.svg';
+import iconBranch from '../svg/icon_branch.svg';
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ global.cycleAnalytics.StageTestComponent = Vue.extend({
+ props: {
+ items: Array,
+ stage: Object,
+ },
+ data() {
+ return { iconBuildStatus, iconBranch };
+ },
+ template: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <h5 class="item-title">
+ <span class="icon-build-status">${iconBuildStatus}</span>
+ <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+ &middot;
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="issue-date">
+ {{ build.date }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
deleted file mode 100644
index 4bfd363a1f1..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
+++ /dev/null
@@ -1,44 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
-
- global.cycleAnalytics.StageTestComponent = Vue.extend({
- props: {
- items: Array,
- stage: Object,
- },
- template: `
- <div>
- <div class="events-description">
- {{ stage.description }}
- </div>
- <ul class="stage-event-list">
- <li v-for="build in items" class="stage-event-item item-build-component">
- <div class="item-details">
- <h5 class="item-title">
- <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
- <a :href="build.url" class="item-build-name">{{ build.name }}</a>
- &middot;
- <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
- <i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
- <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
- </h5>
- <span>
- <a :href="build.url" class="issue-date">
- {{ build.date }}
- </a>
- </span>
- </div>
- <div class="item-time">
- <total-time :time="build.totalTime"></total-time>
- </div>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index 0d85e1a4678..0d85e1a4678 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
new file mode 100644
index 00000000000..beff293b587
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -0,0 +1,135 @@
+/* global Vue */
+/* global Cookies */
+/* global Flash */
+
+window.Vue = require('vue');
+window.Cookies = require('js-cookie');
+require('./components/stage_code_component');
+require('./components/stage_issue_component');
+require('./components/stage_plan_component');
+require('./components/stage_production_component');
+require('./components/stage_review_component');
+require('./components/stage_staging_component');
+require('./components/stage_test_component');
+require('./components/total_time_component');
+require('./cycle_analytics_service');
+require('./cycle_analytics_store');
+require('./default_event_objects');
+
+$(() => {
+ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
+ const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
+ const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
+ const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
+ requestPath: cycleAnalyticsEl.dataset.requestPath,
+ });
+
+ gl.cycleAnalyticsApp = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ data: {
+ state: cycleAnalyticsStore.state,
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+ hasError: false,
+ startDate: 30,
+ isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
+ },
+ computed: {
+ currentStage() {
+ return cycleAnalyticsStore.currentActiveStage();
+ },
+ },
+ components: {
+ 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
+ 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
+ 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
+ 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
+ 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
+ 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
+ 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
+ },
+ created() {
+ this.fetchCycleAnalyticsData();
+ },
+ methods: {
+ handleError() {
+ cycleAnalyticsStore.setErrorState(true);
+ return new Flash('There was an error while fetching cycle analytics data.');
+ },
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ this.startDate = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.fetchCycleAnalyticsData({ startDate: this.startDate });
+ });
+ },
+ fetchCycleAnalyticsData(options) {
+ const fetchOptions = options || { startDate: this.startDate };
+
+ this.isLoading = true;
+
+ cycleAnalyticsService
+ .fetchCycleAnalyticsData(fetchOptions)
+ .done((response) => {
+ cycleAnalyticsStore.setCycleAnalyticsData(response);
+ this.selectDefaultStage();
+ this.initDropdown();
+ })
+ .error(() => {
+ this.handleError();
+ })
+ .always(() => {
+ this.isLoading = false;
+ });
+ },
+ selectDefaultStage() {
+ const stage = this.state.stages.first();
+ this.selectStage(stage);
+ },
+ selectStage(stage) {
+ if (this.isLoadingStage) return;
+ if (this.currentStage === stage) return;
+
+ if (!stage.isUserAllowed) {
+ cycleAnalyticsStore.setActiveStage(stage);
+ return;
+ }
+
+ this.isLoadingStage = true;
+ cycleAnalyticsStore.setStageEvents([], stage);
+ cycleAnalyticsStore.setActiveStage(stage);
+
+ cycleAnalyticsService
+ .fetchStageData({
+ stage,
+ startDate: this.startDate,
+ })
+ .done((response) => {
+ this.isEmptyStage = !response.events.length;
+ cycleAnalyticsStore.setStageEvents(response.events, stage);
+ })
+ .error(() => {
+ this.isEmptyStage = true;
+ })
+ .always(() => {
+ this.isLoadingStage = false;
+ });
+ },
+ dismissOverviewDialog() {
+ this.isOverviewDialogDismissed = true;
+ Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
+ },
+ },
+ });
+
+ // Register global components
+ Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
+});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
deleted file mode 100644
index 1ac715aab77..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ /dev/null
@@ -1,128 +0,0 @@
-/* global Vue */
-/* global Cookies */
-/* global Flash */
-
-window.Vue = require('vue');
-window.Cookies = require('js-cookie');
-
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
-
-$(() => {
- const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
- const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
- const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
- const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
- requestPath: cycleAnalyticsEl.dataset.requestPath,
- });
-
- gl.cycleAnalyticsApp = new Vue({
- el: '#cycle-analytics',
- name: 'CycleAnalytics',
- data: {
- state: cycleAnalyticsStore.state,
- isLoading: false,
- isLoadingStage: false,
- isEmptyStage: false,
- hasError: false,
- startDate: 30,
- isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
- },
- computed: {
- currentStage() {
- return cycleAnalyticsStore.currentActiveStage();
- },
- },
- components: {
- 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
- 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
- 'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
- 'stage-test-component': gl.cycleAnalytics.StageTestComponent,
- 'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
- 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
- 'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
- },
- created() {
- this.fetchCycleAnalyticsData();
- },
- methods: {
- handleError() {
- cycleAnalyticsStore.setErrorState(true);
- return new Flash('There was an error while fetching cycle analytics data.');
- },
- initDropdown() {
- const $dropdown = $('.js-ca-dropdown');
- const $label = $dropdown.find('.dropdown-label');
-
- $dropdown.find('li a').off('click').on('click', (e) => {
- e.preventDefault();
- const $target = $(e.currentTarget);
- this.startDate = $target.data('value');
-
- $label.text($target.text().trim());
- this.fetchCycleAnalyticsData({ startDate: this.startDate });
- });
- },
- fetchCycleAnalyticsData(options) {
- const fetchOptions = options || { startDate: this.startDate };
-
- this.isLoading = true;
-
- cycleAnalyticsService
- .fetchCycleAnalyticsData(fetchOptions)
- .done((response) => {
- cycleAnalyticsStore.setCycleAnalyticsData(response);
- this.selectDefaultStage();
- this.initDropdown();
- })
- .error(() => {
- this.handleError();
- })
- .always(() => {
- this.isLoading = false;
- });
- },
- selectDefaultStage() {
- const stage = this.state.stages.first();
- this.selectStage(stage);
- },
- selectStage(stage) {
- if (this.isLoadingStage) return;
- if (this.currentStage === stage) return;
-
- if (!stage.isUserAllowed) {
- cycleAnalyticsStore.setActiveStage(stage);
- return;
- }
-
- this.isLoadingStage = true;
- cycleAnalyticsStore.setStageEvents([], stage);
- cycleAnalyticsStore.setActiveStage(stage);
-
- cycleAnalyticsService
- .fetchStageData({
- stage,
- startDate: this.startDate,
- })
- .done((response) => {
- this.isEmptyStage = !response.events.length;
- cycleAnalyticsStore.setStageEvents(response.events, stage);
- })
- .error(() => {
- this.isEmptyStage = true;
- })
- .always(() => {
- this.isLoadingStage = false;
- });
- },
- dismissOverviewDialog() {
- this.isOverviewDialogDismissed = true;
- Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
- },
- },
- });
-
- // Register global components
- Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
-});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index 9f74b14c4b9..9f74b14c4b9 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
new file mode 100644
index 00000000000..7ae9de7297c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -0,0 +1,104 @@
+/* eslint-disable no-param-reassign */
+
+require('../lib/utils/text_utility');
+const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
+
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+
+ const EMPTY_STAGE_TEXTS = {
+ issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
+ plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
+ code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
+ test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
+ review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
+ staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
+ production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
+ };
+
+ global.cycleAnalytics.CycleAnalyticsStore = {
+ state: {
+ summary: '',
+ stats: '',
+ analytics: '',
+ events: [],
+ stages: [],
+ },
+ setCycleAnalyticsData(data) {
+ this.state = Object.assign(this.state, this.decorateData(data));
+ },
+ decorateData(data) {
+ const newData = {};
+
+ newData.stages = data.stats || [];
+ newData.summary = data.summary || [];
+
+ newData.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ newData.stages.forEach((item) => {
+ const stageSlug = gl.text.dasherize(item.title.toLowerCase());
+ item.active = false;
+ item.isUserAllowed = data.permissions[stageSlug];
+ item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
+ item.component = `stage-${stageSlug}-component`;
+ item.slug = stageSlug;
+ });
+ newData.analytics = data;
+ return newData;
+ },
+ setLoadingState(state) {
+ this.state.isLoading = state;
+ },
+ setErrorState(state) {
+ this.state.hasError = state;
+ },
+ deactivateAllStages() {
+ this.state.stages.forEach((stage) => {
+ stage.active = false;
+ });
+ },
+ setActiveStage(stage) {
+ this.deactivateAllStages();
+ stage.active = true;
+ },
+ setStageEvents(events, stage) {
+ this.state.events = this.decorateEvents(events, stage);
+ },
+ decorateEvents(events, stage) {
+ const newEvents = [];
+
+ events.forEach((item) => {
+ if (!item) return;
+
+ const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
+
+ eventItem.totalTime = eventItem.total_time;
+
+ if (eventItem.author) {
+ eventItem.author.webUrl = eventItem.author.web_url;
+ eventItem.author.avatarUrl = eventItem.author.avatar_url;
+ }
+
+ if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
+ if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
+ if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
+
+ delete eventItem.author.web_url;
+ delete eventItem.author.avatar_url;
+ delete eventItem.total_time;
+ delete eventItem.created_at;
+ delete eventItem.short_sha;
+ delete eventItem.commit_url;
+
+ newEvents.push(eventItem);
+ });
+
+ return newEvents;
+ },
+ currentActiveStage() {
+ return this.state.stages.find(stage => stage.active);
+ },
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
deleted file mode 100644
index 3efeb141008..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-require('../lib/utils/text_utility');
-const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
-
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
-
- const EMPTY_STAGE_TEXTS = {
- issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
- plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
- code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
- test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
- review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
- staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
- production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
- };
-
- global.cycleAnalytics.CycleAnalyticsStore = {
- state: {
- summary: '',
- stats: '',
- analytics: '',
- events: [],
- stages: [],
- },
- setCycleAnalyticsData(data) {
- this.state = Object.assign(this.state, this.decorateData(data));
- },
- decorateData(data) {
- const newData = {};
-
- newData.stages = data.stats || [];
- newData.summary = data.summary || [];
-
- newData.summary.forEach((item) => {
- item.value = item.value || '-';
- });
-
- newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.title.toLowerCase());
- item.active = false;
- item.isUserAllowed = data.permissions[stageSlug];
- item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
- item.component = `stage-${stageSlug}-component`;
- item.slug = stageSlug;
- });
- newData.analytics = data;
- return newData;
- },
- setLoadingState(state) {
- this.state.isLoading = state;
- },
- setErrorState(state) {
- this.state.hasError = state;
- },
- deactivateAllStages() {
- this.state.stages.forEach((stage) => {
- stage.active = false;
- });
- },
- setActiveStage(stage) {
- this.deactivateAllStages();
- stage.active = true;
- },
- setStageEvents(events, stage) {
- this.state.events = this.decorateEvents(events, stage);
- },
- decorateEvents(events, stage) {
- const newEvents = [];
-
- events.forEach((item) => {
- if (!item) return;
-
- const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
-
- eventItem.totalTime = eventItem.total_time;
- eventItem.author.webUrl = eventItem.author.web_url;
- eventItem.author.avatarUrl = eventItem.author.avatar_url;
-
- if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
- if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
- if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
-
- delete eventItem.author.web_url;
- delete eventItem.author.avatar_url;
- delete eventItem.total_time;
- delete eventItem.created_at;
- delete eventItem.short_sha;
- delete eventItem.commit_url;
-
- newEvents.push(eventItem);
- });
-
- return newEvents;
- },
- currentActiveStage() {
- return this.state.stages.find(stage => stage.active);
- },
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js
index cfaf9835bf8..cfaf9835bf8 100644
--- a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6
+++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
deleted file mode 100644
index 5d486bcaf66..00000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
- global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg
new file mode 100644
index 00000000000..9f547d3d744
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
deleted file mode 100644
index 661bf9e9f1c..00000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
- global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg
new file mode 100644
index 00000000000..b932d90618a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
deleted file mode 100644
index 2208c27a619..00000000000
--- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
- global.cycleAnalytics = global.cycleAnalytics || {};
- global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
-
- global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg
new file mode 100644
index 00000000000..6a517756058
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
new file mode 100644
index 00000000000..cfa60325fcc
--- /dev/null
+++ b/app/assets/javascripts/diff.js
@@ -0,0 +1,128 @@
+/* eslint-disable class-methods-use-this */
+
+require('./lib/utils/url_utility');
+
+const UNFOLD_COUNT = 20;
+let isBound = false;
+
+class Diff {
+ constructor() {
+ const $diffFile = $('.files .diff-file');
+ $diffFile.singleFileDiff();
+ $diffFile.filesCommentButton();
+
+ $diffFile.each((index, file) => new gl.ImageFile(file));
+
+ if (this.diffViewType() === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
+
+ if (!isBound) {
+ $(document)
+ .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ isBound = true;
+ }
+
+ if (gl.utils.getLocationHash()) {
+ this.highlightSelectedLine();
+ }
+
+ this.openAnchoredDiff();
+ }
+
+ handleClickUnfold(e) {
+ const $target = $(e.target);
+ // current babel config relies on iterators implementation, so we cannot simply do:
+ // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
+ const ref = this.lineNumbers($target.parent());
+ const oldLineNumber = ref[0];
+ const newLineNumber = ref[1];
+ const offset = newLineNumber - oldLineNumber;
+ const bottom = $target.hasClass('js-unfold-bottom');
+ let since;
+ let to;
+ let unfold = true;
+
+ if (bottom) {
+ const lineNumber = newLineNumber + 1;
+ since = lineNumber;
+ to = lineNumber + UNFOLD_COUNT;
+ } else {
+ const lineNumber = newLineNumber - 1;
+ since = lineNumber - UNFOLD_COUNT;
+ to = lineNumber;
+
+ // make sure we aren't loading more than we need
+ const prevNewLine = this.lineNumbers($target.parent().prev())[1];
+ if (since <= prevNewLine + 1) {
+ since = prevNewLine + 1;
+ unfold = false;
+ }
+ }
+
+ const file = $target.parents('.diff-file');
+ const link = file.data('blob-diff-path');
+ const view = file.data('view');
+
+ const params = { since, to, bottom, offset, unfold, view };
+ $.get(link, params, response => $target.parent().replaceWith(response));
+ }
+
+ openAnchoredDiff(cb) {
+ const locationHash = gl.utils.getLocationHash();
+ const anchoredDiff = locationHash && locationHash.split('_')[0];
+
+ if (!anchoredDiff) return;
+
+ const diffTitle = $(`#${anchoredDiff}`);
+ const diffFile = diffTitle.closest('.diff-file');
+ const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+ if (nothingHereBlock.length) {
+ const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
+ diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
+ this.highlightSelectedLine();
+ if (cb) cb();
+ });
+ } else if (cb) {
+ cb();
+ }
+ }
+
+ handleClickLineNum(e) {
+ const hash = $(e.currentTarget).attr('href');
+ e.preventDefault();
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+ this.highlightSelectedLine();
+ }
+
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ lineNumbers(line) {
+ if (!line.children().length) {
+ return [0, 0];
+ }
+ return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
+ }
+
+ highlightSelectedLine() {
+ const hash = gl.utils.getLocationHash();
+ const $diffFiles = $('.diff-file');
+ $diffFiles.find('.hll').removeClass('hll');
+
+ if (hash) {
+ $diffFiles
+ .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
+ .addClass('hll');
+ }
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.Diff = Diff;
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
deleted file mode 100644
index ccccd0a36ff..00000000000
--- a/app/assets/javascripts/diff.js.es6
+++ /dev/null
@@ -1,126 +0,0 @@
-/* eslint-disable class-methods-use-this */
-
-require('./lib/utils/url_utility');
-
-(() => {
- const UNFOLD_COUNT = 20;
- let isBound = false;
-
- class Diff {
- constructor() {
- const $diffFile = $('.files .diff-file');
- $diffFile.singleFileDiff();
- $diffFile.filesCommentButton();
-
- $diffFile.each((index, file) => new gl.ImageFile(file));
-
- if (this.diffViewType() === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
-
- if (!isBound) {
- $(document)
- .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
- isBound = true;
- }
-
- this.openAnchoredDiff();
- }
-
- handleClickUnfold(e) {
- const $target = $(e.target);
- // current babel config relies on iterators implementation, so we cannot simply do:
- // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
- const ref = this.lineNumbers($target.parent());
- const oldLineNumber = ref[0];
- const newLineNumber = ref[1];
- const offset = newLineNumber - oldLineNumber;
- const bottom = $target.hasClass('js-unfold-bottom');
- let since;
- let to;
- let unfold = true;
-
- if (bottom) {
- const lineNumber = newLineNumber + 1;
- since = lineNumber;
- to = lineNumber + UNFOLD_COUNT;
- } else {
- const lineNumber = newLineNumber - 1;
- since = lineNumber - UNFOLD_COUNT;
- to = lineNumber;
-
- // make sure we aren't loading more than we need
- const prevNewLine = this.lineNumbers($target.parent().prev())[1];
- if (since <= prevNewLine + 1) {
- since = prevNewLine + 1;
- unfold = false;
- }
- }
-
- const file = $target.parents('.diff-file');
- const link = file.data('blob-diff-path');
- const view = file.data('view');
-
- const params = { since, to, bottom, offset, unfold, view };
- $.get(link, params, response => $target.parent().replaceWith(response));
- }
-
- openAnchoredDiff(cb) {
- const locationHash = gl.utils.getLocationHash();
- const anchoredDiff = locationHash && locationHash.split('_')[0];
-
- if (!anchoredDiff) return;
-
- const diffTitle = $(`#${anchoredDiff}`);
- const diffFile = diffTitle.closest('.diff-file');
- const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
- if (nothingHereBlock.length) {
- const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
- diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
- this.highlighSelectedLine();
- if (cb) cb();
- });
- } else if (cb) {
- cb();
- }
- }
-
- handleClickLineNum(e) {
- const hash = $(e.currentTarget).attr('href');
- e.preventDefault();
- if (window.history.pushState) {
- window.history.pushState(null, null, hash);
- } else {
- window.location.hash = hash;
- }
- this.highlighSelectedLine();
- }
-
- diffViewType() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
-
- lineNumbers(line) {
- if (!line.children().length) {
- return [0, 0];
- }
- return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
- }
-
- highlighSelectedLine() {
- const hash = gl.utils.getLocationHash();
- const $diffFiles = $('.diff-file');
- $diffFiles.find('.hll').removeClass('hll');
-
- if (hash) {
- $diffFiles
- .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
- .addClass('hll');
- }
- }
- }
-
- window.gl = window.gl || {};
- window.gl.Diff = Diff;
-})();
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index d948dff58ec..d948dff58ec 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
new file mode 100644
index 00000000000..dd7081aefb7
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -0,0 +1,156 @@
+/* global CommentsStore Cookies notes */
+import Vue from 'vue';
+import collapseIcon from '../icons/collapse_icon.svg';
+
+(() => {
+ const DiffNoteAvatars = Vue.extend({
+ props: ['discussionId'],
+ data() {
+ return {
+ isVisible: false,
+ lineType: '',
+ storeState: CommentsStore.state,
+ shownAvatars: 3,
+ collapseIcon,
+ };
+ },
+ template: `
+ <div class="diff-comment-avatar-holders"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <img v-for="note in notesSubset"
+ class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
+ width="19"
+ height="19"
+ role="button"
+ data-container="body"
+ data-placement="top"
+ data-html="true"
+ :data-line-type="lineType"
+ :title="note.authorName + ': ' + note.noteTruncated"
+ :src="note.authorAvatar"
+ @click="clickedAvatar($event)" />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
+ :data-line-type="lineType"
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
+ </div>
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+ });
+ });
+ },
+ destroyed() {
+ $(document).off('toggle.comments');
+ },
+ watch: {
+ storeState: {
+ handler() {
+ this.$nextTick(() => {
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
+ });
+ },
+ deep: true,
+ },
+ },
+ computed: {
+ notesSubset() {
+ let notes = [];
+
+ if (this.discussion) {
+ notes = Object.keys(this.discussion.notes)
+ .slice(0, this.shownAvatars)
+ .map(noteId => this.discussion.notes[noteId]);
+ }
+
+ return notes;
+ },
+ extraNotesTitle() {
+ if (this.discussion) {
+ const extra = this.discussion.notesCount() - this.shownAvatars;
+
+ return `${extra} more comment${extra > 1 ? 's' : ''}`;
+ }
+
+ return '';
+ },
+ discussion() {
+ return this.storeState[this.discussionId];
+ },
+ notesCount() {
+ if (this.discussion) {
+ return this.discussion.notesCount();
+ }
+
+ return 0;
+ },
+ moreText() {
+ const plusSign = this.notesCount < 100 ? '+' : '';
+
+ return `${plusSign}${this.notesCount - this.shownAvatars}`;
+ },
+ },
+ methods: {
+ clickedAvatar(e) {
+ notes.addDiffNote(e);
+
+ // Toggle the active state of the toggle all button
+ this.toggleDiscussionsToggleState();
+
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('hide');
+ });
+ },
+ addNoCommentClass() {
+ const notesCount = this.notesCount;
+
+ $(this.$el).closest('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0)
+ .nextUntil('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0);
+ },
+ toggleDiscussionsToggleState() {
+ const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
+ const $visibleNotesHolders = $notesHolders.filter(':visible');
+ const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
+
+ $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
+ },
+ setDiscussionVisible() {
+ this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
+ },
+ },
+ });
+
+ Vue.component('diff-note-avatars', DiffNoteAvatars);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 283dc330cad..283dc330cad 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
new file mode 100644
index 00000000000..e86bef47172
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
@@ -0,0 +1,29 @@
+/* global Vue */
+/* global CommentsStore */
+
+(() => {
+ const NewIssueForDiscussion = Vue.extend({
+ props: {
+ discussionId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion() {
+ return this.discussions[this.discussionId];
+ },
+ showButton() {
+ if (this.discussion) return !this.discussion.isResolved();
+ return false;
+ },
+ },
+ });
+
+ Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
new file mode 100644
index 00000000000..fbd980f0fce
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -0,0 +1,120 @@
+/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
+/* global CommentsStore */
+/* global ResolveService */
+/* global Flash */
+const Vue = require('vue');
+
+(() => {
+ const ResolveBtn = Vue.extend({
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ canResolve: Boolean,
+ resolvedBy: String,
+ authorName: String,
+ authorAvatar: String,
+ noteTruncated: String,
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false,
+ note: {},
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
+ }
+ },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
+ },
+ },
+ methods: {
+ updateTooltip: function () {
+ this.$nextTick(() => {
+ $(this.$refs.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ });
+ },
+ resolve: function () {
+ if (!this.canResolve) return;
+
+ let promise;
+ this.loading = true;
+
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.noteId);
+ }
+
+ promise.then((response) => {
+ this.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
+ }
+
+ this.updateTooltip();
+ });
+ }
+ },
+ mounted: function () {
+ $(this.$refs.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
+
+ this.note = this.discussion.getNote(this.noteId);
+ }
+ });
+
+ Vue.component('resolve-btn', ResolveBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
deleted file mode 100644
index d1873d6c7a2..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ /dev/null
@@ -1,108 +0,0 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
-/* global CommentsStore */
-/* global ResolveService */
-/* global Flash */
-const Vue = require('vue');
-
-(() => {
- const ResolveBtn = Vue.extend({
- props: {
- noteId: Number,
- discussionId: String,
- resolved: Boolean,
- canResolve: Boolean,
- resolvedBy: String
- },
- data: function () {
- return {
- discussions: CommentsStore.state,
- loading: false,
- note: {},
- };
- },
- watch: {
- 'discussions': {
- handler: 'updateTooltip',
- deep: true
- }
- },
- computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
- buttonText: function () {
- if (this.isResolved) {
- return `Resolved by ${this.resolvedByName}`;
- } else if (this.canResolve) {
- return 'Mark as resolved';
- } else {
- return 'Unable to resolve';
- }
- },
- isResolved: function () {
- if (this.note) {
- return this.note.resolved;
- } else {
- return false;
- }
- },
- resolvedByName: function () {
- return this.note.resolved_by;
- },
- },
- methods: {
- updateTooltip: function () {
- this.$nextTick(() => {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('fixTitle');
- });
- },
- resolve: function () {
- if (!this.canResolve) return;
-
- let promise;
- this.loading = true;
-
- if (this.isResolved) {
- promise = ResolveService
- .unresolve(this.noteId);
- } else {
- promise = ResolveService
- .resolve(this.noteId);
- }
-
- promise.then((response) => {
- this.loading = false;
-
- if (response.status === 200) {
- const data = response.json();
- const resolved_by = data ? data.resolved_by : null;
-
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
- this.discussion.updateHeadline(data);
- } else {
- new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
- }
-
- this.updateTooltip();
- });
- }
- },
- mounted: function () {
- $(this.$refs.button).tooltip({
- container: 'body'
- });
- },
- beforeDestroy: function () {
- CommentsStore.delete(this.discussionId, this.noteId);
- },
- created: function () {
- CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
-
- this.note = this.discussion.getNote(this.noteId);
- }
- });
-
- Vue.component('resolve-btn', ResolveBtn);
-})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js
index de9367f2136..de9367f2136 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 7c5fcd04d2d..7c5fcd04d2d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
new file mode 100644
index 00000000000..4f6b86a917c
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -0,0 +1,68 @@
+/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
+/* global Vue */
+/* global ResolveCount */
+
+const Vue = require('vue');
+require('./models/discussion');
+require('./models/note');
+require('./stores/comments');
+require('./services/resolve');
+require('./mixins/discussion');
+require('./components/comment_resolve_btn');
+require('./components/jump_to_discussion');
+require('./components/resolve_btn');
+require('./components/resolve_count');
+require('./components/resolve_discussion_btn');
+require('./components/diff_note_avatars');
+require('./components/new_issue_for_discussion');
+
+$(() => {
+ const projectPath = document.querySelector('.merge-request').dataset.projectPath;
+ const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
+
+ window.gl = window.gl || {};
+ window.gl.diffNoteApps = {};
+
+ window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
+
+ gl.diffNotesCompileComponents = () => {
+ $('diff-note-avatars').each(function () {
+ const tmp = Vue.extend({
+ template: $(this).get(0).outerHTML
+ });
+ const tmpApp = new tmp().$mount();
+
+ $(this).replaceWith(tmpApp.$el);
+ });
+
+ const $components = $(COMPONENT_SELECTOR).filter(function () {
+ return $(this).closest('resolve-count').length !== 1;
+ });
+
+ if ($components) {
+ $components.each(function () {
+ const $this = $(this);
+ const noteId = $this.attr(':note-id');
+ const tmp = Vue.extend({
+ template: $this.get(0).outerHTML
+ });
+ const tmpApp = new tmp().$mount();
+
+ if (noteId) {
+ gl.diffNoteApps[`note_${noteId}`] = tmpApp;
+ }
+
+ $this.replaceWith(tmpApp.$el);
+ });
+ }
+ };
+
+ gl.diffNotesCompileComponents();
+
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ }
+ });
+});
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
deleted file mode 100644
index 190461451d5..00000000000
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
-/* global Vue */
-/* global ResolveCount */
-
-function requireAll(context) { return context.keys().map(context); }
-const Vue = require('vue');
-requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
-
-$(() => {
- const projectPath = document.querySelector('.merge-request').dataset.projectPath;
- const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
-
- window.gl = window.gl || {};
- window.gl.diffNoteApps = {};
-
- window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
-
- gl.diffNotesCompileComponents = () => {
- const $components = $(COMPONENT_SELECTOR).filter(function () {
- return $(this).closest('resolve-count').length !== 1;
- });
-
- if ($components) {
- $components.each(function () {
- const $this = $(this);
- const noteId = $this.attr(':note-id');
- const tmp = Vue.extend({
- template: $this.get(0).outerHTML
- });
- const tmpApp = new tmp().$mount();
-
- if (noteId) {
- gl.diffNoteApps[`note_${noteId}`] = tmpApp;
- }
-
- $this.replaceWith(tmpApp.$el);
- });
- }
- };
-
- gl.diffNotesCompileComponents();
-
- new Vue({
- el: '#resolve-count-app',
- components: {
- 'resolve-count': ResolveCount
- }
- });
-});
diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
new file mode 100644
index 00000000000..bd4b393cfaa
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
@@ -0,0 +1 @@
+<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js
index 3c08c222f46..3c08c222f46 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
new file mode 100644
index 00000000000..dce1a9b58bd
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -0,0 +1,96 @@
+/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* global Vue */
+/* global NoteModel */
+
+class DiscussionModel {
+ constructor (discussionId) {
+ this.id = discussionId;
+ this.notes = {};
+ this.loading = false;
+ this.canResolve = false;
+ }
+
+ createNote (noteObj) {
+ Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
+ }
+
+ deleteNote (noteId) {
+ Vue.delete(this.notes, noteId);
+ }
+
+ getNote (noteId) {
+ return this.notes[noteId];
+ }
+
+ notesCount() {
+ return Object.keys(this.notes).length;
+ }
+
+ isResolved () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ resolveAllNotes (resolved_by) {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ note.resolved = true;
+ note.resolved_by = resolved_by;
+ }
+ }
+ }
+
+ unResolveAllNotes () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.resolved) {
+ note.resolved = false;
+ note.resolved_by = null;
+ }
+ }
+ }
+
+ updateHeadline (data) {
+ const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
+ const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
+
+ if (data.discussion_headline_html) {
+ if ($discussionHeadline.length) {
+ $discussionHeadline.replaceWith(data.discussion_headline_html);
+ } else {
+ $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
+ }
+
+ gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
+ } else {
+ $discussionHeadline.remove();
+ }
+ }
+
+ isResolvable () {
+ if (!this.canResolve) {
+ return false;
+ }
+
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.canResolve) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
deleted file mode 100644
index fa518ba4d33..00000000000
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
-/* global Vue */
-/* global NoteModel */
-
-class DiscussionModel {
- constructor (discussionId) {
- this.id = discussionId;
- this.notes = {};
- this.loading = false;
- this.canResolve = false;
- }
-
- createNote (noteId, canResolve, resolved, resolved_by) {
- Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
- }
-
- deleteNote (noteId) {
- Vue.delete(this.notes, noteId);
- }
-
- getNote (noteId) {
- return this.notes[noteId];
- }
-
- notesCount() {
- return Object.keys(this.notes).length;
- }
-
- isResolved () {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (!note.resolved) {
- return false;
- }
- }
- return true;
- }
-
- resolveAllNotes (resolved_by) {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (!note.resolved) {
- note.resolved = true;
- note.resolved_by = resolved_by;
- }
- }
- }
-
- unResolveAllNotes () {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (note.resolved) {
- note.resolved = false;
- note.resolved_by = null;
- }
- }
- }
-
- updateHeadline (data) {
- const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
- const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
-
- if (data.discussion_headline_html) {
- if ($discussionHeadline.length) {
- $discussionHeadline.replaceWith(data.discussion_headline_html);
- } else {
- $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
- }
-
- gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
- } else {
- $discussionHeadline.remove();
- }
- }
-
- isResolvable () {
- if (!this.canResolve) {
- return false;
- }
-
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (note.canResolve) {
- return true;
- }
- }
-
- return false;
- }
-}
-
-window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
new file mode 100644
index 00000000000..04465aa507e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js
@@ -0,0 +1,16 @@
+/* eslint-disable camelcase, no-unused-vars */
+
+class NoteModel {
+ constructor(discussionId, noteObj) {
+ this.discussionId = discussionId;
+ this.id = noteObj.noteId;
+ this.canResolve = noteObj.canResolve;
+ this.resolved = noteObj.resolved;
+ this.resolved_by = noteObj.resolvedBy;
+ this.authorName = noteObj.authorName;
+ this.authorAvatar = noteObj.authorAvatar;
+ this.noteTruncated = noteObj.noteTruncated;
+ }
+}
+
+window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
deleted file mode 100644
index f3a7cba5ef6..00000000000
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-/* eslint-disable camelcase, no-unused-vars */
-
-class NoteModel {
- constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
- this.discussionId = discussionId;
- this.id = noteId;
- this.canResolve = canResolve;
- this.resolved = resolved;
- this.resolved_by = resolved_by;
- }
-}
-
-window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js
index 090c454e9e4..090c454e9e4 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
new file mode 100644
index 00000000000..69c4d7a8434
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -0,0 +1,57 @@
+/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* global Vue */
+/* global DiscussionModel */
+
+((w) => {
+ w.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
+
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
+
+ return discussion;
+ },
+ create: function (noteObj) {
+ const discussion = this.createDiscussion(noteObj.discussionId);
+
+ discussion.createNote(noteObj);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ const ids = [];
+
+ for (const discussionId in this.state) {
+ const discussion = this.state[discussionId];
+
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
+ }
+ }
+
+ return ids;
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
deleted file mode 100644
index c80d979b977..00000000000
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ /dev/null
@@ -1,57 +0,0 @@
-/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
-/* global Vue */
-/* global DiscussionModel */
-
-((w) => {
- w.CommentsStore = {
- state: {},
- get: function (discussionId, noteId) {
- return this.state[discussionId].getNote(noteId);
- },
- createDiscussion: function (discussionId, canResolve) {
- let discussion = this.state[discussionId];
- if (!this.state[discussionId]) {
- discussion = new DiscussionModel(discussionId);
- Vue.set(this.state, discussionId, discussion);
- }
-
- if (canResolve !== undefined) {
- discussion.canResolve = canResolve;
- }
-
- return discussion;
- },
- create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
- const discussion = this.createDiscussion(discussionId);
-
- discussion.createNote(noteId, canResolve, resolved, resolved_by);
- },
- update: function (discussionId, noteId, resolved, resolved_by) {
- const discussion = this.state[discussionId];
- const note = discussion.getNote(noteId);
- note.resolved = resolved;
- note.resolved_by = resolved_by;
- },
- delete: function (discussionId, noteId) {
- const discussion = this.state[discussionId];
- discussion.deleteNote(noteId);
-
- if (discussion.notesCount() === 0) {
- Vue.delete(this.state, discussionId);
- }
- },
- unresolvedDiscussionIds: function () {
- const ids = [];
-
- for (const discussionId in this.state) {
- const discussion = this.state[discussionId];
-
- if (!discussion.isResolved()) {
- ids.push(discussion.id);
- }
- }
-
- return ids;
- }
- };
-})(window);
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
new file mode 100644
index 00000000000..3557f6f617e
--- /dev/null
+++ b/app/assets/javascripts/dispatcher.js
@@ -0,0 +1,448 @@
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
+/* global UsernameValidator */
+/* global ActiveTabMemoizer */
+/* global ShortcutsNavigation */
+/* global Build */
+/* global Issuable */
+/* global ShortcutsIssuable */
+/* global ZenMode */
+/* global Milestone */
+/* global IssuableForm */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global MergedButtons */
+/* global Commit */
+/* global NotificationsForm */
+/* global TreeView */
+/* global NotificationsDropdown */
+/* global UsersSelect */
+/* global GroupAvatar */
+/* global LineHighlighter */
+/* global ProjectFork */
+/* global BuildArtifacts */
+/* global GroupsSelect */
+/* global Search */
+/* global Admin */
+/* global NamespaceSelects */
+/* global ShortcutsDashboardNavigation */
+/* global Project */
+/* global ProjectAvatar */
+/* global CompareAutocomplete */
+/* global ProjectNew */
+/* global Star */
+/* global ProjectShow */
+/* global Labels */
+/* global Shortcuts */
+import Issue from './issue';
+
+import BindInOut from './behaviors/bind_in_out';
+import GroupName from './group_name';
+import GroupsList from './groups_list';
+import ProjectsList from './projects_list';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
+import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
+
+const ShortcutsBlob = require('./shortcuts_blob');
+const UserCallout = require('./user_callout');
+
+(function() {
+ var Dispatcher;
+
+ $(function() {
+ return new Dispatcher();
+ });
+
+ Dispatcher = (function() {
+ function Dispatcher() {
+ this.initSearch();
+ this.initFieldErrors();
+ this.initPageScripts();
+ }
+
+ Dispatcher.prototype.initPageScripts = function() {
+ var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
+ page = $('body').attr('data-page');
+ if (!page) {
+ return false;
+ }
+ path = page.split(':');
+ shortcut_handler = null;
+
+ function initBlob() {
+ new LineHighlighter();
+
+ new BlobLinePermalinkUpdater(
+ document.querySelector('#blob-content-holder'),
+ '.diff-line-num[data-line-number]',
+ document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+ );
+
+ shortcut_handler = new ShortcutsNavigation();
+ fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
+ }
+
+ switch (page) {
+ case 'sessions:new':
+ new UsernameValidator();
+ new ActiveTabMemoizer();
+ break;
+ case 'projects:boards:show':
+ case 'projects:boards:index':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:builds:show':
+ new Build();
+ break;
+ case 'projects:merge_requests:index':
+ case 'projects:issues:index':
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+ });
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:issues:show':
+ new Issue();
+ shortcut_handler = new ShortcutsIssuable();
+ new ZenMode();
+ break;
+ case 'projects:milestones:show':
+ case 'groups:milestones:show':
+ case 'dashboard:milestones:show':
+ new Milestone();
+ break;
+ case 'dashboard:todos:index':
+ new gl.Todos();
+ break;
+ case 'dashboard:projects:index':
+ case 'dashboard:projects:starred':
+ case 'explore:projects:index':
+ case 'explore:projects:trending':
+ case 'explore:projects:starred':
+ case 'admin:projects:index':
+ new ProjectsList();
+ break;
+ case 'dashboard:groups:index':
+ case 'explore:groups:index':
+ new GroupsList();
+ break;
+ case 'projects:milestones:new':
+ case 'projects:milestones:edit':
+ case 'projects:milestones:update':
+ new ZenMode();
+ new gl.DueDateSelectors();
+ new gl.GLForm($('.milestone-form'));
+ break;
+ case 'groups:milestones:new':
+ new ZenMode();
+ break;
+ case 'projects:compare:show':
+ new gl.Diff();
+ break;
+ case 'projects:branches:index':
+ gl.AjaxLoadingSpinner.init();
+ break;
+ case 'projects:issues:new':
+ case 'projects:issues:edit':
+ shortcut_handler = new ShortcutsNavigation();
+ new gl.GLForm($('.issue-form'));
+ new IssuableForm($('.issue-form'));
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
+ break;
+ case 'projects:merge_requests:new':
+ case 'projects:merge_requests:new_diffs':
+ case 'projects:merge_requests:edit':
+ new gl.Diff();
+ shortcut_handler = new ShortcutsNavigation();
+ new gl.GLForm($('.merge-request-form'));
+ new IssuableForm($('.merge-request-form'));
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
+ break;
+ case 'projects:tags:new':
+ new ZenMode();
+ new gl.GLForm($('.tag-form'));
+ break;
+ case 'projects:releases:edit':
+ new ZenMode();
+ new gl.GLForm($('.release-form'));
+ break;
+ case 'projects:merge_requests:show':
+ new gl.Diff();
+ shortcut_handler = new ShortcutsIssuable(true);
+ new ZenMode();
+ new MergedButtons();
+ break;
+ case 'projects:merge_requests:commits':
+ new MergedButtons();
+ break;
+ case "projects:merge_requests:diffs":
+ new gl.Diff();
+ new ZenMode();
+ new MergedButtons();
+ break;
+ case 'dashboard:activity':
+ new gl.Activities();
+ break;
+ case 'projects:commit:show':
+ new Commit();
+ new gl.Diff();
+ new ZenMode();
+ shortcut_handler = new ShortcutsNavigation();
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
+ break;
+ case 'projects:commit:pipelines':
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
+ break;
+ case 'projects:commits:show':
+ case 'projects:activity':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new NotificationsForm();
+ if ($('#tree-slider').length) {
+ new TreeView();
+ }
+ break;
+ case 'projects:pipelines:builds':
+ case 'projects:pipelines:show':
+ const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
+
+ new gl.Pipelines({
+ initTabs: true,
+ tabsOptions: {
+ action: controllerAction,
+ defaultAction: 'pipelines',
+ parentEl: '.pipelines-tabs',
+ },
+ });
+ break;
+ case 'groups:activity':
+ new gl.Activities();
+ break;
+ case 'groups:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new NotificationsForm();
+ new NotificationsDropdown();
+ new ProjectsList();
+ break;
+ case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
+ new gl.Members();
+ new UsersSelect();
+ break;
+ case 'projects:members:show':
+ new gl.MemberExpirationDate('.js-access-expiration-date-groups');
+ new GroupsSelect();
+ new gl.MemberExpirationDate();
+ new gl.Members();
+ new UsersSelect();
+ break;
+ case 'groups:new':
+ case 'admin:groups:new':
+ case 'groups:create':
+ case 'admin:groups:create':
+ BindInOut.initAll();
+ case 'groups:new':
+ case 'admin:groups:new':
+ case 'groups:edit':
+ case 'admin:groups:edit':
+ new GroupAvatar();
+ break;
+ case 'projects:tree:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new TreeView();
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:find_file:show':
+ shortcut_handler = true;
+ break;
+ case 'projects:blob:new':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:blob:create':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:blob:show':
+ gl.TargetBranchDropDown.bootstrap();
+ initBlob();
+ break;
+ case 'projects:blob:edit':
+ gl.TargetBranchDropDown.bootstrap();
+ break;
+ case 'projects:blame:show':
+ initBlob();
+ break;
+ case 'groups:labels:new':
+ case 'groups:labels:edit':
+ case 'projects:labels:new':
+ case 'projects:labels:edit':
+ new Labels();
+ break;
+ case 'projects:labels:index':
+ if ($('.prioritized-labels').length) {
+ new gl.LabelManager();
+ }
+ break;
+ case 'projects:network:show':
+ // Ensure we don't create a particular shortcut handler here. This is
+ // already created, where the network graph is created.
+ shortcut_handler = true;
+ break;
+ case 'projects:forks:new':
+ new ProjectFork();
+ break;
+ case 'projects:artifacts:browse':
+ new BuildArtifacts();
+ break;
+ case 'help:index':
+ gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
+ break;
+ case 'search:show':
+ new Search();
+ break;
+ case 'projects:repository:show':
+ new gl.ProtectedBranchCreate();
+ new gl.ProtectedBranchEditList();
+ break;
+ case 'projects:ci_cd:show':
+ new gl.ProjectVariables();
+ break;
+ case 'ci:lints:create':
+ case 'ci:lints:show':
+ new gl.CILintEditor();
+ break;
+ case 'users:show':
+ new UserCallout();
+ break;
+ }
+ switch (path.first()) {
+ case 'sessions':
+ case 'omniauth_callbacks':
+ if (!gon.u2f) break;
+ gl.u2fAuthenticate = new gl.U2FAuthenticate(
+ $('#js-authenticate-u2f'),
+ '#js-login-u2f-form',
+ gon.u2f,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ gl.u2fAuthenticate.start();
+ case 'admin':
+ new Admin();
+ switch (path[1]) {
+ case 'groups':
+ new UsersSelect();
+ break;
+ case 'projects':
+ new NamespaceSelects();
+ break;
+ case 'labels':
+ switch (path[2]) {
+ case 'new':
+ case 'edit':
+ new Labels();
+ }
+ case 'abuse_reports':
+ new gl.AbuseReports();
+ break;
+ }
+ break;
+ case 'dashboard':
+ case 'root':
+ shortcut_handler = new ShortcutsDashboardNavigation();
+ new UserCallout();
+ break;
+ case 'groups':
+ new GroupName();
+ break;
+ case 'profiles':
+ new NotificationsForm();
+ new NotificationsDropdown();
+ break;
+ case 'projects':
+ new Project();
+ new ProjectAvatar();
+ new GroupName();
+ switch (path[1]) {
+ case 'compare':
+ new CompareAutocomplete();
+ break;
+ case 'edit':
+ shortcut_handler = new ShortcutsNavigation();
+ new ProjectNew();
+ break;
+ case 'new':
+ new ProjectNew();
+ break;
+ case 'show':
+ new Star();
+ new ProjectNew();
+ new ProjectShow();
+ new NotificationsDropdown();
+ break;
+ case 'wikis':
+ new gl.Wikis();
+ shortcut_handler = new ShortcutsNavigation();
+ new ZenMode();
+ new gl.GLForm($('.wiki-form'));
+ break;
+ case 'snippets':
+ shortcut_handler = new ShortcutsNavigation();
+ if (path[2] === 'show') {
+ new ZenMode();
+ }
+ break;
+ case 'labels':
+ case 'graphs':
+ case 'compare':
+ case 'pipelines':
+ case 'forks':
+ case 'milestones':
+ case 'project_members':
+ case 'deploy_keys':
+ case 'builds':
+ case 'hooks':
+ case 'services':
+ case 'protected_branches':
+ shortcut_handler = new ShortcutsNavigation();
+ }
+ }
+ // If we haven't installed a custom shortcut handler, install the default one
+ if (!shortcut_handler) {
+ new Shortcuts();
+ }
+ };
+
+ Dispatcher.prototype.initSearch = function() {
+ // Only when search form is present
+ if ($('.search').length) {
+ return new gl.SearchAutocomplete();
+ }
+ };
+
+ Dispatcher.prototype.initFieldErrors = function() {
+ $('.gl-show-field-errors').each((i, form) => {
+ new gl.GlFieldErrors(form);
+ });
+ };
+
+ return Dispatcher;
+ })();
+}).call(window);
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
deleted file mode 100644
index f55db02f0fd..00000000000
--- a/app/assets/javascripts/dispatcher.js.es6
+++ /dev/null
@@ -1,386 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
-/* global UsernameValidator */
-/* global ActiveTabMemoizer */
-/* global ShortcutsNavigation */
-/* global Build */
-/* global Issuable */
-/* global Issue */
-/* global ShortcutsIssuable */
-/* global ZenMode */
-/* global Milestone */
-/* global IssuableForm */
-/* global LabelsSelect */
-/* global MilestoneSelect */
-/* global MergedButtons */
-/* global Commit */
-/* global NotificationsForm */
-/* global TreeView */
-/* global NotificationsDropdown */
-/* global UsersSelect */
-/* global GroupAvatar */
-/* global LineHighlighter */
-/* global ProjectFork */
-/* global BuildArtifacts */
-/* global GroupsSelect */
-/* global Search */
-/* global Admin */
-/* global NamespaceSelects */
-/* global ShortcutsDashboardNavigation */
-/* global Project */
-/* global ProjectAvatar */
-/* global CompareAutocomplete */
-/* global ProjectNew */
-/* global Star */
-/* global ProjectShow */
-/* global Labels */
-/* global Shortcuts */
-
-const ShortcutsBlob = require('./shortcuts_blob');
-
-(function() {
- var Dispatcher;
-
- $(function() {
- return new Dispatcher();
- });
-
- Dispatcher = (function() {
- function Dispatcher() {
- this.initSearch();
- this.initFieldErrors();
- this.initPageScripts();
- }
-
- Dispatcher.prototype.initPageScripts = function() {
- var page, path, shortcut_handler;
- page = $('body').attr('data-page');
- if (!page) {
- return false;
- }
- path = page.split(':');
- shortcut_handler = null;
- switch (page) {
- case 'sessions:new':
- new UsernameValidator();
- new ActiveTabMemoizer();
- break;
- case 'projects:boards:show':
- case 'projects:boards:index':
- shortcut_handler = new ShortcutsNavigation();
- break;
- case 'projects:builds:show':
- new Build();
- break;
- case 'projects:merge_requests:index':
- case 'projects:issues:index':
- if (gl.FilteredSearchManager) {
- new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
- }
- Issuable.init();
- new gl.IssuableBulkActions({
- prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
- });
- shortcut_handler = new ShortcutsNavigation();
- break;
- case 'projects:issues:show':
- new Issue();
- shortcut_handler = new ShortcutsIssuable();
- new ZenMode();
- break;
- case 'projects:milestones:show':
- case 'groups:milestones:show':
- case 'dashboard:milestones:show':
- new Milestone();
- break;
- case 'dashboard:todos:index':
- new gl.Todos();
- break;
- case 'projects:milestones:new':
- case 'projects:milestones:edit':
- case 'projects:milestones:update':
- new ZenMode();
- new gl.DueDateSelectors();
- new gl.GLForm($('.milestone-form'));
- break;
- case 'groups:milestones:new':
- new ZenMode();
- break;
- case 'projects:compare:show':
- new gl.Diff();
- break;
- case 'projects:issues:new':
- case 'projects:issues:edit':
- shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.issue-form'));
- new IssuableForm($('.issue-form'));
- new LabelsSelect();
- new MilestoneSelect();
- new gl.IssuableTemplateSelectors();
- break;
- case 'projects:merge_requests:new':
- case 'projects:merge_requests:new_diffs':
- case 'projects:merge_requests:edit':
- new gl.Diff();
- shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.merge-request-form'));
- new IssuableForm($('.merge-request-form'));
- new LabelsSelect();
- new MilestoneSelect();
- new gl.IssuableTemplateSelectors();
- break;
- case 'projects:tags:new':
- new ZenMode();
- new gl.GLForm($('.tag-form'));
- break;
- case 'projects:releases:edit':
- new ZenMode();
- new gl.GLForm($('.release-form'));
- break;
- case 'projects:merge_requests:show':
- new gl.Diff();
- shortcut_handler = new ShortcutsIssuable(true);
- new ZenMode();
- new MergedButtons();
- break;
- case 'projects:merge_requests:commits':
- new MergedButtons();
- break;
- case "projects:merge_requests:diffs":
- new gl.Diff();
- new ZenMode();
- new MergedButtons();
- break;
- case 'dashboard:activity':
- new gl.Activities();
- break;
- case 'dashboard:projects:starred':
- new gl.Activities();
- break;
- case 'projects:commit:show':
- new Commit();
- new gl.Diff();
- new ZenMode();
- shortcut_handler = new ShortcutsNavigation();
- break;
- case 'projects:commit:pipelines':
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- }).bindEvents();
- break;
- case 'projects:commits:show':
- case 'projects:activity':
- shortcut_handler = new ShortcutsNavigation();
- break;
- case 'projects:show':
- shortcut_handler = new ShortcutsNavigation();
- new NotificationsForm();
- if ($('#tree-slider').length) {
- new TreeView();
- }
- break;
- case 'projects:pipelines:builds':
- case 'projects:pipelines:show':
- const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
-
- new gl.Pipelines({
- initTabs: true,
- tabsOptions: {
- action: controllerAction,
- defaultAction: 'pipelines',
- parentEl: '.pipelines-tabs',
- },
- });
- break;
- case 'groups:activity':
- new gl.Activities();
- break;
- case 'groups:show':
- shortcut_handler = new ShortcutsNavigation();
- new NotificationsForm();
- new NotificationsDropdown();
- break;
- case 'groups:group_members:index':
- new gl.MemberExpirationDate();
- new gl.Members();
- new UsersSelect();
- break;
- case 'projects:members:show':
- new gl.MemberExpirationDate('.js-access-expiration-date-groups');
- new GroupsSelect();
- new gl.MemberExpirationDate();
- new gl.Members();
- new UsersSelect();
- break;
- case 'groups:new':
- case 'groups:edit':
- case 'admin:groups:edit':
- case 'admin:groups:new':
- new GroupAvatar();
- break;
- case 'projects:tree:show':
- shortcut_handler = new ShortcutsNavigation();
- new TreeView();
- break;
- case 'projects:find_file:show':
- shortcut_handler = true;
- break;
- case 'projects:blob:show':
- case 'projects:blame:show':
- new LineHighlighter();
- shortcut_handler = new ShortcutsNavigation();
- const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
- new ShortcutsBlob({
- skipResetBindings: true,
- fileBlobPermalinkUrl,
- });
- break;
- case 'groups:labels:new':
- case 'groups:labels:edit':
- case 'projects:labels:new':
- case 'projects:labels:edit':
- new Labels();
- break;
- case 'projects:labels:index':
- if ($('.prioritized-labels').length) {
- new gl.LabelManager();
- }
- break;
- case 'projects:network:show':
- // Ensure we don't create a particular shortcut handler here. This is
- // already created, where the network graph is created.
- shortcut_handler = true;
- break;
- case 'projects:forks:new':
- new ProjectFork();
- break;
- case 'projects:artifacts:browse':
- new BuildArtifacts();
- break;
- case 'help:index':
- gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
- break;
- case 'search:show':
- new Search();
- break;
- case 'projects:protected_branches:index':
- new gl.ProtectedBranchCreate();
- new gl.ProtectedBranchEditList();
- break;
- case 'projects:ci_cd:show':
- new gl.ProjectVariables();
- break;
- case 'ci:lints:create':
- case 'ci:lints:show':
- new gl.CILintEditor();
- break;
- }
- switch (path.first()) {
- case 'sessions':
- case 'omniauth_callbacks':
- if (!gon.u2f) break;
- gl.u2fAuthenticate = new gl.U2FAuthenticate(
- $('#js-authenticate-u2f'),
- '#js-login-u2f-form',
- gon.u2f,
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- gl.u2fAuthenticate.start();
- case 'admin':
- new Admin();
- switch (path[1]) {
- case 'groups':
- new UsersSelect();
- break;
- case 'projects':
- new NamespaceSelects();
- break;
- case 'labels':
- switch (path[2]) {
- case 'new':
- case 'edit':
- new Labels();
- }
- case 'abuse_reports':
- new gl.AbuseReports();
- break;
- }
- break;
- case 'dashboard':
- case 'root':
- shortcut_handler = new ShortcutsDashboardNavigation();
- break;
- case 'profiles':
- new NotificationsForm();
- new NotificationsDropdown();
- break;
- case 'projects':
- new Project();
- new ProjectAvatar();
- switch (path[1]) {
- case 'compare':
- new CompareAutocomplete();
- break;
- case 'edit':
- shortcut_handler = new ShortcutsNavigation();
- new ProjectNew();
- break;
- case 'new':
- new ProjectNew();
- break;
- case 'show':
- new Star();
- new ProjectNew();
- new ProjectShow();
- new NotificationsDropdown();
- break;
- case 'wikis':
- new gl.Wikis();
- shortcut_handler = new ShortcutsNavigation();
- new ZenMode();
- new gl.GLForm($('.wiki-form'));
- break;
- case 'snippets':
- shortcut_handler = new ShortcutsNavigation();
- if (path[2] === 'show') {
- new ZenMode();
- }
- break;
- case 'labels':
- case 'graphs':
- case 'compare':
- case 'pipelines':
- case 'forks':
- case 'milestones':
- case 'project_members':
- case 'deploy_keys':
- case 'builds':
- case 'hooks':
- case 'services':
- case 'protected_branches':
- shortcut_handler = new ShortcutsNavigation();
- }
- }
- // If we haven't installed a custom shortcut handler, install the default one
- if (!shortcut_handler) {
- new Shortcuts();
- }
- };
-
- Dispatcher.prototype.initSearch = function() {
- // Only when search form is present
- if ($('.search').length) {
- return new gl.SearchAutocomplete();
- }
- };
-
- Dispatcher.prototype.initFieldErrors = function() {
- $('.gl-show-field-errors').each((i, form) => {
- new gl.GlFieldErrors(form);
- });
- };
-
- return Dispatcher;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index 5cdf11c6a2c..020f8b4ac65 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
- self.hook.list[config.method].call(self.hook.list, data);
+ if (!self.destroyed) {
+ self.hook.list[config.method].call(self.hook.list, data);
+ }
},
init: function init(hook) {
var self = this;
+ self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
@@ -71,6 +74,9 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
self._loadData(d, config, self);
+ }, function(xhrError) {
+ // TODO: properly handle errors due to XHR cancellation
+ return;
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
@@ -79,6 +85,7 @@ require('../window')(function(w){
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index b63d73066cb..05eba7aef56 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -82,6 +82,9 @@ require('../window')(function(w){
this._loadUrlData(url)
.then(function(data) {
self._loadData(data, config, self);
+ }, function(xhrError) {
+ // TODO: properly handle errors due to XHR cancellation
+ return;
});
}
},
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 64a7a9eaf37..f2963a5eb19 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -3,217 +3,216 @@
require('./preview_markdown');
-(function() {
- this.DropzoneInput = (function() {
- function DropzoneInput(form) {
- var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
- Dropzone.autoDiscover = false;
- alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
- alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
- divHover = "<div class=\"div-dropzone-hover\"></div>";
- divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
- divAlert = "<div class=\"" + alertClass + "\"></div>";
- iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
- iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
- uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
- btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
- project_uploads_path = window.project_uploads_path || null;
- max_file_size = gon.max_file_size || 10;
- form_textarea = $(form).find(".js-gfm-input");
- form_textarea.wrap("<div class=\"div-dropzone\"></div>");
- form_textarea.on('paste', (function(_this) {
- return function(event) {
- return handlePaste(event);
- };
- })(this));
- $mdArea = $(form_textarea).closest('.md-area');
- $(form).setupMarkdownPreview();
- form_dropzone = $(form).find('.div-dropzone');
- form_dropzone.parent().addClass("div-dropzone-wrapper");
- form_dropzone.append(divHover);
- form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
- form_dropzone.append(divSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- dropzone = form_dropzone.dropzone({
- url: project_uploads_path,
- dictDefaultMessage: "",
- clickable: true,
- paramName: "file",
- maxFilesize: max_file_size,
- uploadMultiple: false,
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- previewContainer: false,
- processing: function() {
- return $(".div-dropzone-alert").alert("close");
- },
- dragover: function() {
- $mdArea.addClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0.7);
- },
- dragleave: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- },
- drop: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- form_textarea.focus();
- },
- success: function(header, response) {
- pasteText(response.link.markdown);
- },
- error: function(temp) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
- }
- },
- totaluploadprogress: function(totalUploadProgress) {
- uploadProgress.text(Math.round(totalUploadProgress) + "%");
- },
- sending: function() {
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
- },
- queuecomplete: function() {
- uploadProgress.text("");
- $(".dz-preview").remove();
- $(".markdown-area").trigger("input");
- $(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- }
- });
- child = $(dropzone[0]).children("textarea");
- handlePaste = function(event) {
- var filename, image, pasteEvent, text;
- pasteEvent = event.originalEvent;
- if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
- image = isImage(pasteEvent);
- if (image) {
- event.preventDefault();
- filename = getFilename(pasteEvent) || "image.png";
- text = "{{" + filename + "}}";
- pasteText(text);
- return uploadFile(image.getAsFile(), filename);
- }
- }
+window.DropzoneInput = (function() {
+ function DropzoneInput(form) {
+ var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+ Dropzone.autoDiscover = false;
+ alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
+ alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
+ divHover = "<div class=\"div-dropzone-hover\"></div>";
+ divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
+ divAlert = "<div class=\"" + alertClass + "\"></div>";
+ iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
+ iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
+ uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
+ btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
+ project_uploads_path = window.project_uploads_path || null;
+ max_file_size = gon.max_file_size || 10;
+ form_textarea = $(form).find(".js-gfm-input");
+ form_textarea.wrap("<div class=\"div-dropzone\"></div>");
+ form_textarea.on('paste', (function(_this) {
+ return function(event) {
+ return handlePaste(event);
};
- isImage = function(data) {
- var i, item;
- i = 0;
- while (i < data.clipboardData.items.length) {
- item = data.clipboardData.items[i];
- if (item.type.indexOf("image") !== -1) {
- return item;
- }
- i += 1;
- }
- return false;
- };
- pasteText = function(text) {
- var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
- caretStart = $(child)[0].selectionStart;
- caretEnd = $(child)[0].selectionEnd;
- textEnd = $(child).val().length;
- beforeSelection = $(child).val().substring(0, caretStart);
- afterSelection = $(child).val().substring(caretEnd, textEnd);
- $(child).val(beforeSelection + text + afterSelection);
- child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length);
- return form_textarea.trigger("input");
- };
- getFilename = function(e) {
- var value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData("Text");
- } else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData("text/plain");
+ })(this));
+ $mdArea = $(form_textarea).closest('.md-area');
+ $(form).setupMarkdownPreview();
+ form_dropzone = $(form).find('.div-dropzone');
+ form_dropzone.parent().addClass("div-dropzone-wrapper");
+ form_dropzone.append(divHover);
+ form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
+ form_dropzone.append(divSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
+ form_dropzone.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
+ });
+ dropzone = form_dropzone.dropzone({
+ url: project_uploads_path,
+ dictDefaultMessage: "",
+ clickable: true,
+ paramName: "file",
+ maxFilesize: max_file_size,
+ uploadMultiple: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ previewContainer: false,
+ processing: function() {
+ return $(".div-dropzone-alert").alert("close");
+ },
+ dragover: function() {
+ $mdArea.addClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0.7);
+ },
+ dragleave: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ },
+ drop: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ form_textarea.focus();
+ },
+ success: function(header, response) {
+ pasteText(response.link.markdown);
+ },
+ error: function(temp) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
}
- value = value.split("\r");
- return value.first();
- };
- uploadFile = function(item, filename) {
- var formData;
- formData = new FormData();
- formData.append("file", item, filename);
- return $.ajax({
- url: project_uploads_path,
- type: "POST",
- data: formData,
- dataType: "json",
- processData: false,
- contentType: false,
- headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
- },
- beforeSend: function() {
- showSpinner();
- return closeAlertMessage();
- },
- success: function(e, textStatus, response) {
- return insertToTextArea(filename, response.responseJSON.link.markdown);
- },
- error: function(response) {
- return showError(response.responseJSON.message);
- },
- complete: function() {
- return closeSpinner();
- }
- });
- };
- insertToTextArea = function(filename, url) {
- return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url + "\n");
- });
- };
- appendToTextArea = function(url) {
- return $(child).val(function(index, val) {
- return val + url + "\n";
- });
- };
- showSpinner = function(e) {
- return form.find(".div-dropzone-spinner").css({
+ },
+ totaluploadprogress: function(totalUploadProgress) {
+ uploadProgress.text(Math.round(totalUploadProgress) + "%");
+ },
+ sending: function() {
+ form_dropzone.find(".div-dropzone-spinner").css({
"opacity": 0.7,
"display": "inherit"
});
- };
- closeSpinner = function() {
- return form.find(".div-dropzone-spinner").css({
+ },
+ queuecomplete: function() {
+ uploadProgress.text("");
+ $(".dz-preview").remove();
+ $(".markdown-area").trigger("input");
+ $(".div-dropzone-spinner").css({
"opacity": 0,
"display": "none"
});
- };
- showError = function(message) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- return $(".div-dropzone-alert").append(btnAlert + message);
+ }
+ });
+ child = $(dropzone[0]).children("textarea");
+ handlePaste = function(event) {
+ var filename, image, pasteEvent, text;
+ pasteEvent = event.originalEvent;
+ if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
+ image = isImage(pasteEvent);
+ if (image) {
+ event.preventDefault();
+ filename = getFilename(pasteEvent) || "image.png";
+ text = "{{" + filename + "}}";
+ pasteText(text);
+ return uploadFile(image.getAsFile(), filename);
}
- };
- closeAlertMessage = function() {
- return form.find(".div-dropzone-alert").alert("close");
- };
- form.find(".markdown-selector").click(function(e) {
- e.preventDefault();
- $(this).closest('.gfm-form').find('.div-dropzone').click();
+ }
+ };
+ isImage = function(data) {
+ var i, item;
+ i = 0;
+ while (i < data.clipboardData.items.length) {
+ item = data.clipboardData.items[i];
+ if (item.type.indexOf("image") !== -1) {
+ return item;
+ }
+ i += 1;
+ }
+ return false;
+ };
+ pasteText = function(text) {
+ var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
+ var formattedText = text + "\n\n";
+ caretStart = $(child)[0].selectionStart;
+ caretEnd = $(child)[0].selectionEnd;
+ textEnd = $(child).val().length;
+ beforeSelection = $(child).val().substring(0, caretStart);
+ afterSelection = $(child).val().substring(caretEnd, textEnd);
+ $(child).val(beforeSelection + formattedText + afterSelection);
+ child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ return form_textarea.trigger("input");
+ };
+ getFilename = function(e) {
+ var value;
+ if (window.clipboardData && window.clipboardData.getData) {
+ value = window.clipboardData.getData("Text");
+ } else if (e.clipboardData && e.clipboardData.getData) {
+ value = e.clipboardData.getData("text/plain");
+ }
+ value = value.split("\r");
+ return value.first();
+ };
+ uploadFile = function(item, filename) {
+ var formData;
+ formData = new FormData();
+ formData.append("file", item, filename);
+ return $.ajax({
+ url: project_uploads_path,
+ type: "POST",
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ beforeSend: function() {
+ showSpinner();
+ return closeAlertMessage();
+ },
+ success: function(e, textStatus, response) {
+ return insertToTextArea(filename, response.responseJSON.link.markdown);
+ },
+ error: function(response) {
+ return showError(response.responseJSON.message);
+ },
+ complete: function() {
+ return closeSpinner();
+ }
+ });
+ };
+ insertToTextArea = function(filename, url) {
+ return $(child).val(function(index, val) {
+ return val.replace("{{" + filename + "}}", url + "\n");
+ });
+ };
+ appendToTextArea = function(url) {
+ return $(child).val(function(index, val) {
+ return val + url + "\n";
+ });
+ };
+ showSpinner = function(e) {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0.7,
+ "display": "inherit"
+ });
+ };
+ closeSpinner = function() {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
});
- }
+ };
+ showError = function(message) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ return $(".div-dropzone-alert").append(btnAlert + message);
+ }
+ };
+ closeAlertMessage = function() {
+ return form.find(".div-dropzone-alert").alert("close");
+ };
+ form.find(".markdown-selector").click(function(e) {
+ e.preventDefault();
+ $(this).closest('.gfm-form').find('.div-dropzone').click();
+ });
+ }
- return DropzoneInput;
- })();
-}).call(window);
+ return DropzoneInput;
+})();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
new file mode 100644
index 00000000000..db10b383913
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js
@@ -0,0 +1,203 @@
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
+/* global dateFormat */
+/* global Pikaday */
+
+class DueDateSelect {
+ constructor({ $dropdown, $loading } = {}) {
+ const $dropdownParent = $dropdown.closest('.dropdown');
+ const $block = $dropdown.closest('.block');
+ this.$loading = $loading;
+ this.$dropdown = $dropdown;
+ this.$dropdownParent = $dropdownParent;
+ this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
+ this.$block = $block;
+ this.$selectbox = $dropdown.closest('.selectbox');
+ this.$value = $block.find('.value');
+ this.$valueContent = $block.find('.value-content');
+ this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
+ this.fieldName = $dropdown.data('field-name'),
+ this.abilityName = $dropdown.data('ability-name'),
+ this.issueUpdateURL = $dropdown.data('issue-update');
+
+ this.rawSelectedDate = null;
+ this.displayedDate = null;
+ this.datePayload = null;
+
+ this.initGlDropdown();
+ this.initRemoveDueDate();
+ this.initDatePicker();
+ }
+
+ initGlDropdown() {
+ this.$dropdown.glDropdown({
+ opened: () => {
+ const calendar = this.$datePicker.data('pikaday');
+ calendar.show();
+ },
+ hidden: () => {
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+ }
+ });
+ }
+
+ initDatePicker() {
+ const $dueDateInput = $(`input[name='${this.fieldName}']`);
+
+ const calendar = new Pikaday({
+ field: $dueDateInput.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ onSelect: (dateText) => {
+ const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
+
+ $dueDateInput.val(formattedDate);
+
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
+ this.updateIssueBoardIssue();
+ } else {
+ this.saveDueDate(true);
+ }
+ }
+ });
+
+ calendar.setDate(new Date($dueDateInput.val()));
+ this.$datePicker.append(calendar.el);
+ this.$datePicker.data('pikaday', calendar);
+ }
+
+ initRemoveDueDate() {
+ this.$block.on('click', '.js-remove-due-date', (e) => {
+ const calendar = this.$datePicker.data('pikaday');
+ e.preventDefault();
+
+ calendar.setDate(null);
+
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ this.updateIssueBoardIssue();
+ } else {
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
+ }
+ });
+ }
+
+ saveDueDate(isDropdown) {
+ this.parseSelectedDate();
+ this.prepSelectedDate();
+ this.submitSelectedDate(isDropdown);
+ }
+
+ parseSelectedDate() {
+ this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
+
+ if (this.rawSelectedDate.length) {
+ // Construct Date object manually to avoid buggy dateString support within Date constructor
+ const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
+ const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
+ this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
+ } else {
+ this.displayedDate = 'No due date';
+ }
+ }
+
+ prepSelectedDate() {
+ const datePayload = {};
+ datePayload[this.abilityName] = {};
+ datePayload[this.abilityName].due_date = this.rawSelectedDate;
+ this.datePayload = datePayload;
+ }
+
+ updateIssueBoardIssue () {
+ this.$loading.fadeIn();
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+
+ gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ .then(() => {
+ this.$loading.fadeOut();
+ });
+ }
+
+ submitSelectedDate(isDropdown) {
+ return $.ajax({
+ type: 'PUT',
+ url: this.issueUpdateURL,
+ data: this.datePayload,
+ dataType: 'json',
+ beforeSend: () => {
+ const selectedDateValue = this.datePayload[this.abilityName].due_date;
+ const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+
+ this.$loading.removeClass('hidden').fadeIn();
+
+ if (isDropdown) {
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ }
+
+ this.$value.css('display', '');
+ this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
+ this.$sidebarValue.html(this.displayedDate);
+
+ return selectedDateValue.length ?
+ $('.js-remove-due-date-holder').removeClass('hidden') :
+ $('.js-remove-due-date-holder').addClass('hidden');
+ }
+ }).done((data) => {
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ return this.$loading.fadeOut();
+ });
+ }
+}
+
+class DueDateSelectors {
+ constructor() {
+ this.initMilestoneDatePicker();
+ this.initIssuableSelect();
+ }
+
+ initMilestoneDatePicker() {
+ $('.datepicker').each(function() {
+ const $datePicker = $(this);
+ const calendar = new Pikaday({
+ field: $datePicker.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ onSelect(dateText) {
+ $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
+ });
+ calendar.setDate(new Date($datePicker.val()));
+
+ $datePicker.data('pikaday', calendar);
+ });
+
+ $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ e.preventDefault();
+ const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ calendar.setDate(null);
+ });
+ }
+
+ initIssuableSelect() {
+ const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+
+ $('.js-due-date-select').each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ new DueDateSelect({
+ $dropdown,
+ $loading
+ });
+ });
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.DueDateSelectors = DueDateSelectors;
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
deleted file mode 100644
index 9169fcd7328..00000000000
--- a/app/assets/javascripts/due_date_select.js.es6
+++ /dev/null
@@ -1,204 +0,0 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
-/* global dateFormat */
-/* global Pikaday */
-
-(function(global) {
- class DueDateSelect {
- constructor({ $dropdown, $loading } = {}) {
- const $dropdownParent = $dropdown.closest('.dropdown');
- const $block = $dropdown.closest('.block');
- this.$loading = $loading;
- this.$dropdown = $dropdown;
- this.$dropdownParent = $dropdownParent;
- this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
- this.$block = $block;
- this.$selectbox = $dropdown.closest('.selectbox');
- this.$value = $block.find('.value');
- this.$valueContent = $block.find('.value-content');
- this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
- this.fieldName = $dropdown.data('field-name'),
- this.abilityName = $dropdown.data('ability-name'),
- this.issueUpdateURL = $dropdown.data('issue-update');
-
- this.rawSelectedDate = null;
- this.displayedDate = null;
- this.datePayload = null;
-
- this.initGlDropdown();
- this.initRemoveDueDate();
- this.initDatePicker();
- }
-
- initGlDropdown() {
- this.$dropdown.glDropdown({
- opened: () => {
- const calendar = this.$datePicker.data('pikaday');
- calendar.show();
- },
- hidden: () => {
- this.$selectbox.hide();
- this.$value.css('display', '');
- }
- });
- }
-
- initDatePicker() {
- const $dueDateInput = $(`input[name='${this.fieldName}']`);
-
- const calendar = new Pikaday({
- field: $dueDateInput.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- onSelect: (dateText) => {
- const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
-
- $dueDateInput.val(formattedDate);
-
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
- this.updateIssueBoardIssue();
- } else {
- this.saveDueDate(true);
- }
- }
- });
-
- calendar.setDate(new Date($dueDateInput.val()));
- this.$datePicker.append(calendar.el);
- this.$datePicker.data('pikaday', calendar);
- }
-
- initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
- const calendar = this.$datePicker.data('pikaday');
- e.preventDefault();
-
- calendar.setDate(null);
-
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
- this.updateIssueBoardIssue();
- } else {
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
- }
- });
- }
-
- saveDueDate(isDropdown) {
- this.parseSelectedDate();
- this.prepSelectedDate();
- this.submitSelectedDate(isDropdown);
- }
-
- parseSelectedDate() {
- this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
-
- if (this.rawSelectedDate.length) {
- // Construct Date object manually to avoid buggy dateString support within Date constructor
- const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
- const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
- this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
- } else {
- this.displayedDate = 'No due date';
- }
- }
-
- prepSelectedDate() {
- const datePayload = {};
- datePayload[this.abilityName] = {};
- datePayload[this.abilityName].due_date = this.rawSelectedDate;
- this.datePayload = datePayload;
- }
-
- updateIssueBoardIssue () {
- this.$loading.fadeIn();
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- this.$value.css('display', '');
-
- gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
- .then(() => {
- this.$loading.fadeOut();
- });
- }
-
- submitSelectedDate(isDropdown) {
- return $.ajax({
- type: 'PUT',
- url: this.issueUpdateURL,
- data: this.datePayload,
- dataType: 'json',
- beforeSend: () => {
- const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
-
- this.$loading.fadeIn();
-
- if (isDropdown) {
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- }
-
- this.$value.css('display', '');
- this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
- this.$sidebarValue.html(this.displayedDate);
-
- return selectedDateValue.length ?
- $('.js-remove-due-date-holder').removeClass('hidden') :
- $('.js-remove-due-date-holder').addClass('hidden');
- }
- }).done((data) => {
- if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
- }
- return this.$loading.fadeOut();
- });
- }
- }
-
- class DueDateSelectors {
- constructor() {
- this.initMilestoneDatePicker();
- this.initIssuableSelect();
- }
-
- initMilestoneDatePicker() {
- $('.datepicker').each(function() {
- const $datePicker = $(this);
- const calendar = new Pikaday({
- field: $datePicker.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- onSelect(dateText) {
- $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
- calendar.setDate(new Date($datePicker.val()));
-
- $datePicker.data('pikaday', calendar);
- });
-
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
- e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
- calendar.setDate(null);
- });
- }
-
- initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
-
- $('.js-due-date-select').each((i, dropdown) => {
- const $dropdown = $(dropdown);
- new DueDateSelect({
- $dropdown,
- $loading
- });
- });
- }
- }
-
- global.DueDateSelectors = DueDateSelectors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
new file mode 100644
index 00000000000..51aab8460f6
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -0,0 +1,192 @@
+/* eslint-disable no-new */
+/* global Flash */
+import Vue from 'vue';
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from './environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import eventHub from '../event_hub';
+
+export default Vue.component('environment-component', {
+
+ components: {
+ 'environment-table': EnvironmentTable,
+ 'table-pagination': TablePaginationComponent,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-list-view').dataset;
+ const store = new EnvironmentsStore();
+
+ return {
+ store,
+ state: store.state,
+ visibility: 'available',
+ isLoading: false,
+ cssContainerClass: environmentsData.cssClass,
+ endpoint: environmentsData.environmentsDataEndpoint,
+ canCreateDeployment: environmentsData.canCreateDeployment,
+ canReadEnvironment: environmentsData.canReadEnvironment,
+ canCreateEnvironment: environmentsData.canCreateEnvironment,
+ projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
+ projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+
+ // Pagination Properties,
+ paginationInformation: {},
+ pageNumber: 1,
+ };
+ },
+
+ computed: {
+ scope() {
+ return gl.utils.getParameterByName('scope');
+ },
+
+ canReadEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
+ },
+
+ canCreateDeploymentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
+ },
+
+ canCreateEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
+ },
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ this.service = new EnvironmentsService(this.endpoint);
+
+ this.fetchEnvironments();
+
+ eventHub.$on('refreshEnvironments', this.fetchEnvironments);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshEnvironments');
+ },
+
+ methods: {
+ toggleRow(model) {
+ return this.store.toggleFolder(model.name);
+ },
+
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ * @return {String}
+ */
+ changePage(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
+ },
+
+ fetchEnvironments() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.isLoading = true;
+
+ return this.service.get(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.');
+ });
+ },
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul v-if="!isLoading" class="nav-links">
+ <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
+ <a :href="newEnvironmentPath" class="btn btn-create">
+ New environment
+ </a>
+ </div>
+ </div>
+
+ <div class="content-list environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New Environment
+ </a>
+ </div>
+
+ <div class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :service="service"/>
+ </div>
+
+ <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation">
+ </table-pagination>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
deleted file mode 100644
index 4b700a39d44..00000000000
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ /dev/null
@@ -1,193 +0,0 @@
-/* eslint-disable no-param-reassign, no-new */
-/* global Flash */
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('./environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
-module.exports = Vue.component('environment-component', {
-
- components: {
- 'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
- },
-
- data() {
- const environmentsData = document.querySelector('#environments-list-view').dataset;
- const store = new EnvironmentsStore();
-
- return {
- store,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- endpoint: environmentsData.environmentsDataEndpoint,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
- canCreateEnvironment: environmentsData.canCreateEnvironment,
- projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
- projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- commitIconSvg: environmentsData.commitIconSvg,
- playIconSvg: environmentsData.playIconSvg,
- terminalIconSvg: environmentsData.terminalIconSvg,
-
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return gl.utils.getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- canCreateEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
- },
-
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- const service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return service.all()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
- },
-
- methods: {
- toggleRow(model) {
- return this.store.toggleFolder(model.name);
- },
-
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- * @return {String}
- */
- changePage(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
- },
-
- template: `
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul v-if="!isLoading" class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-create">
- New environment
- </a>
- </div>
- </div>
-
- <div class="environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
- </a>
- </p>
-
- <a v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New Environment
- </a>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg">
- </environment-table>
-
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
- </div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
new file mode 100644
index 00000000000..455a8819549
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -0,0 +1,71 @@
+/* global Flash */
+/* eslint-disable no-new */
+
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ data-toggle="dropdown"
+ :disabled="isLoading">
+ <span>
+ <span v-html="playIconSvg"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </span>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ @click="onClickAction(action.play_path)"
+ class="js-manual-action-link no-btn">
+ ${playIconSvg}
+ <span>
+ {{action.name}}
+ </span>
+ </button>
+ </li>
+ </ul>
+ </button>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
deleted file mode 100644
index c5a714d9673..00000000000
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ /dev/null
@@ -1,43 +0,0 @@
-const Vue = require('vue');
-
-module.exports = Vue.component('actions-component', {
- props: {
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
-
- playIconSvg: {
- type: String,
- required: false,
- },
- },
-
- template: `
- <div class="inline">
- <div class="dropdown">
- <a class="dropdown-new btn btn-default" data-toggle="dropdown">
- <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
- <i class="fa fa-caret-down"></i>
- </a>
-
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for="action in actions">
- <a :href="action.play_path"
- data-method="post"
- rel="nofollow"
- class="js-manual-action-link">
-
- <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
-
- <span>
- {{action.name}}
- </span>
- </a>
- </li>
- </ul>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
new file mode 100644
index 00000000000..b4f9eb357fd
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -0,0 +1,22 @@
+/**
+ * Renders the external url link in environments table.
+ */
+export default {
+ props: {
+ externalUrl: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a
+ class="btn external_url"
+ :href="externalUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ title="Environment external URL">
+ <i class="fa fa-external-link" aria-hidden="true"></i>
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
deleted file mode 100644
index 2599bba3c59..00000000000
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Renders the external url link in environments table.
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('external-url-component', {
- props: {
- externalUrl: {
- type: String,
- default: '',
- },
- },
-
- template: `
- <a class="btn external_url" :href="externalUrl" target="_blank">
- <i class="fa fa-external-link"></i>
- </a>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
new file mode 100644
index 00000000000..66ed10e19d1
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -0,0 +1,514 @@
+import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
+import ActionsComponent from './environment_actions';
+import ExternalUrlComponent from './environment_external_url';
+import StopComponent from './environment_stop';
+import RollbackComponent from './environment_rollback';
+import TerminalButtonComponent from './environment_terminal_button';
+import CommitComponent from '../../vue_shared/components/commit';
+
+/**
+ * Envrionment Item Component
+ *
+ * Renders a table row for each environment.
+ */
+const timeagoInstance = new Timeago();
+
+export default {
+ components: {
+ 'commit-component': CommitComponent,
+ 'actions-component': ActionsComponent,
+ 'external-url-component': ExternalUrlComponent,
+ 'stop-component': StopComponent,
+ 'rollback-component': RollbackComponent,
+ 'terminal-button-component': TerminalButtonComponent,
+ },
+
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ /**
+ * Verifies if `last_deployment` key exists in the current Envrionment.
+ * This key is required to render most of the html - this method works has
+ * an helper.
+ *
+ * @returns {Boolean}
+ */
+ hasLastDeploymentKey() {
+ if (this.model &&
+ this.model.last_deployment &&
+ !this.$options.isObjectEmpty(this.model.last_deployment)) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Verifies is the given environment has manual actions.
+ * Used to verify if we should render them or nor.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ hasManualActions() {
+ return this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.manual_actions &&
+ this.model.last_deployment.manual_actions.length > 0;
+ },
+
+ /**
+ * Returns the value of the `stop_action?` key provided in the response.
+ *
+ * @returns {Boolean}
+ */
+ hasStopAction() {
+ return this.model && this.model['stop_action?'];
+ },
+
+ /**
+ * Verifies if the `deployable` key is present in `last_deployment` key.
+ * Used to verify whether we should or not render the rollback partial.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canRetry() {
+ return this.model &&
+ this.hasLastDeploymentKey &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable;
+ },
+
+ /**
+ * Verifies if the date to be shown is present.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canShowDate() {
+ return this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable !== undefined;
+ },
+
+ /**
+ * Human readable date.
+ *
+ * @returns {String}
+ */
+ createdDate() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.created_at) {
+ return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
+ }
+ return '';
+ },
+
+ /**
+ * Returns the manual actions with the name parsed.
+ *
+ * @returns {Array.<Object>|Undefined}
+ */
+ manualActions() {
+ if (this.hasManualActions) {
+ return this.model.last_deployment.manual_actions.map((action) => {
+ const parsedAction = {
+ name: gl.text.humanize(action.name),
+ play_path: action.play_path,
+ };
+ return parsedAction;
+ });
+ }
+ return [];
+ },
+
+ /**
+ * Builds the string used in the user image alt attribute.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.user &&
+ this.model.last_deployment.user.username) {
+ return `${this.model.last_deployment.user.username}'s avatar'`;
+ }
+ return '';
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.tag) {
+ return this.model.last_deployment.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.ref) {
+ return this.model.last_deployment.ref;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.commit_path) {
+ return this.model.last_deployment.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.short_id) {
+ return this.model.last_deployment.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.title) {
+ return this.model.last_deployment.commit.title;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.author) {
+ return this.model.last_deployment.commit.author;
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `retry_path` key is present and returns its value.
+ *
+ * @returns {String|Undefined}
+ */
+ retryUrl() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path) {
+ return this.model.last_deployment.deployable.retry_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `last?` key is present and returns its value.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isLastDeployment() {
+ return this.model && this.model.last_deployment &&
+ this.model.last_deployment['last?'];
+ },
+
+ /**
+ * Builds the name of the builds needed to display both the name and the id.
+ *
+ * @returns {String}
+ */
+ buildName() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable) {
+ return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
+ }
+ return '';
+ },
+
+ /**
+ * Builds the needed string to show the internal id.
+ *
+ * @returns {String}
+ */
+ deploymentInternalId() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.iid) {
+ return `#${this.model.last_deployment.iid}`;
+ }
+ return '';
+ },
+
+ /**
+ * Verifies if the user object is present under last_deployment object.
+ *
+ * @returns {Boolean}
+ */
+ deploymentHasUser() {
+ return this.model &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user);
+ },
+
+ /**
+ * Returns the user object nested with the last_deployment object.
+ * Used to render the template.
+ *
+ * @returns {Object}
+ */
+ deploymentUser() {
+ if (this.model &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
+ return this.model.last_deployment.user;
+ }
+ return {};
+ },
+
+ /**
+ * Verifies if the build name column should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderBuildName() {
+ return !this.model.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
+ },
+
+ /**
+ * Verifies the presence of all the keys needed to render the buil_path.
+ *
+ * @return {String}
+ */
+ buildPath() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.build_path) {
+ return this.model.last_deployment.deployable.build_path;
+ }
+
+ return '';
+ },
+
+ /**
+ * Verifies the presence of all the keys needed to render the external_url.
+ *
+ * @return {String}
+ */
+ externalURL() {
+ if (this.model && this.model.external_url) {
+ return this.model.external_url;
+ }
+
+ return '';
+ },
+
+ /**
+ * Verifies if deplyment internal ID should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderDeploymentID() {
+ return !this.model.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ this.model.last_deployment.iid !== undefined;
+ },
+
+ environmentPath() {
+ if (this.model && this.model.environment_path) {
+ return this.model.environment_path;
+ }
+
+ return '';
+ },
+
+ /**
+ * Constructs folder URL based on the current location and the folder id.
+ *
+ * @return {String}
+ */
+ folderUrl() {
+ return `${window.location.pathname}/folders/${this.model.folderName}`;
+ },
+
+ },
+
+ /**
+ * Helper to verify if certain given object are empty.
+ * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
+ * @param {Object} object
+ * @returns {Bollean}
+ */
+ isObjectEmpty(object) {
+ for (const key in object) { // eslint-disable-line
+ if (hasOwnProperty.call(object, key)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ template: `
+ <tr>
+ <td>
+ <a v-if="!model.isFolder"
+ class="environment-name"
+ :href="environmentPath">
+ {{model.name}}
+ </a>
+ <a v-else class="folder-name" :href="folderUrl">
+ <span class="folder-icon">
+ <i class="fa fa-folder" aria-hidden="true"></i>
+ </span>
+
+ <span>
+ {{model.folderName}}
+ </span>
+
+ <span class="badge">
+ {{model.size}}
+ </span>
+ </a>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!model.isFolder && deploymentHasUser">
+ by
+ <a :href="deploymentUser.web_url" class="js-deploy-user-container">
+ <img class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="buildPath">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </div>
+ <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span v-if="!model.isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="environments-actions">
+ <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
+ <actions-component v-if="hasManualActions && canCreateDeployment"
+ :service="service"
+ :actions="manualActions"/>
+
+ <external-url-component v-if="externalURL && canReadEnvironment"
+ :external-url="externalURL"/>
+
+ <stop-component v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
+ <terminal-button-component v-if="model && model.terminal_path"
+ :terminal-path="model.terminal_path"/>
+
+ <rollback-component v-if="canRetry && canCreateDeployment"
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl"
+ :service="service"/>
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
deleted file mode 100644
index 24fd58a301a..00000000000
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ /dev/null
@@ -1,549 +0,0 @@
-const Vue = require('vue');
-const Timeago = require('timeago.js');
-
-require('../../lib/utils/text_utility');
-require('../../vue_shared/components/commit');
-const ActionsComponent = require('./environment_actions');
-const ExternalUrlComponent = require('./environment_external_url');
-const StopComponent = require('./environment_stop');
-const RollbackComponent = require('./environment_rollback');
-const TerminalButtonComponent = require('./environment_terminal_button');
-
-/**
- * Envrionment Item Component
- *
- * Renders a table row for each environment.
- */
-
-const timeagoInstance = new Timeago();
-
-module.exports = Vue.component('environment-item', {
-
- components: {
- 'commit-component': gl.CommitComponent,
- 'actions-component': ActionsComponent,
- 'external-url-component': ExternalUrlComponent,
- 'stop-component': StopComponent,
- 'rollback-component': RollbackComponent,
- 'terminal-button-component': TerminalButtonComponent,
- },
-
- props: {
- model: {
- type: Object,
- required: true,
- default: () => ({}),
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- commitIconSvg: {
- type: String,
- required: false,
- },
-
- playIconSvg: {
- type: String,
- required: false,
- },
-
- terminalIconSvg: {
- type: String,
- required: false,
- },
- },
-
- computed: {
- /**
- * Verifies if `last_deployment` key exists in the current Envrionment.
- * This key is required to render most of the html - this method works has
- * an helper.
- *
- * @returns {Boolean}
- */
- hasLastDeploymentKey() {
- if (this.model &&
- this.model.last_deployment &&
- !this.$options.isObjectEmpty(this.model.last_deployment)) {
- return true;
- }
- return false;
- },
-
- /**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0;
- },
-
- /**
- * Returns the value of the `stop_action?` key provided in the response.
- *
- * @returns {Boolean}
- */
- hasStopAction() {
- return this.model && this.model['stop_action?'];
- },
-
- /**
- * Verifies if the `deployable` key is present in `last_deployment` key.
- * Used to verify whether we should or not render the rollback partial.
- *
- * @returns {Boolean|Undefined}
- */
- canRetry() {
- return this.model &&
- this.hasLastDeploymentKey &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable;
- },
-
- /**
- * Verifies if the date to be shown is present.
- *
- * @returns {Boolean|Undefined}
- */
- canShowDate() {
- return this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable !== undefined;
- },
-
- /**
- * Human readable date.
- *
- * @returns {String}
- */
- createdDate() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.created_at) {
- return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
- }
- return '';
- },
-
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map((action) => {
- const parsedAction = {
- name: gl.text.humanize(action.name),
- play_path: action.play_path,
- };
- return parsedAction;
- });
- }
- return [];
- },
-
- /**
- * Builds the string used in the user image alt attribute.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.user &&
- this.model.last_deployment.user.username) {
- return `${this.model.last_deployment.user.username}'s avatar'`;
- }
- return '';
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.tag) {
- return this.model.last_deployment.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.ref) {
- return this.model.last_deployment.ref;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.commit_path) {
- return this.model.last_deployment.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.short_id) {
- return this.model.last_deployment.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.title) {
- return this.model.last_deployment.commit.title;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.author) {
- return this.model.last_deployment.commit.author;
- }
-
- return undefined;
- },
-
- /**
- * Verifies if the `retry_path` key is present and returns its value.
- *
- * @returns {String|Undefined}
- */
- retryUrl() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.retry_path) {
- return this.model.last_deployment.deployable.retry_path;
- }
- return undefined;
- },
-
- /**
- * Verifies if the `last?` key is present and returns its value.
- *
- * @returns {Boolean|Undefined}
- */
- isLastDeployment() {
- return this.model && this.model.last_deployment &&
- this.model.last_deployment['last?'];
- },
-
- /**
- * Builds the name of the builds needed to display both the name and the id.
- *
- * @returns {String}
- */
- buildName() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable) {
- return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
- }
- return '';
- },
-
- /**
- * Builds the needed string to show the internal id.
- *
- * @returns {String}
- */
- deploymentInternalId() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.iid) {
- return `#${this.model.last_deployment.iid}`;
- }
- return '';
- },
-
- /**
- * Verifies if the user object is present under last_deployment object.
- *
- * @returns {Boolean}
- */
- deploymentHasUser() {
- return this.model &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.user);
- },
-
- /**
- * Returns the user object nested with the last_deployment object.
- * Used to render the template.
- *
- * @returns {Object}
- */
- deploymentUser() {
- if (this.model &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
- return this.model.last_deployment.user;
- }
- return {};
- },
-
- /**
- * Verifies if the build name column should be rendered by verifing
- * if all the information needed is present
- * and if the environment is not a folder.
- *
- * @returns {Boolean}
- */
- shouldRenderBuildName() {
- return !this.model.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
- },
-
- /**
- * Verifies the presence of all the keys needed to render the buil_path.
- *
- * @return {String}
- */
- buildPath() {
- if (this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable.build_path) {
- return this.model.last_deployment.deployable.build_path;
- }
-
- return '';
- },
-
- /**
- * Verifies the presence of all the keys needed to render the external_url.
- *
- * @return {String}
- */
- externalURL() {
- if (this.model && this.model.external_url) {
- return this.model.external_url;
- }
-
- return '';
- },
-
- /**
- * Verifies if deplyment internal ID should be rendered by verifing
- * if all the information needed is present
- * and if the environment is not a folder.
- *
- * @returns {Boolean}
- */
- shouldRenderDeploymentID() {
- return !this.model.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- this.model.last_deployment.iid !== undefined;
- },
-
- environmentPath() {
- if (this.model && this.model.environment_path) {
- return this.model.environment_path;
- }
-
- return '';
- },
-
- /**
- * Constructs folder URL based on the current location and the folder id.
- *
- * @return {String}
- */
- folderUrl() {
- return `${window.location.pathname}/folders/${this.model.folderName}`;
- },
-
- },
-
- /**
- * Helper to verify if certain given object are empty.
- * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
- * @param {Object} object
- * @returns {Bollean}
- */
- isObjectEmpty(object) {
- for (const key in object) { // eslint-disable-line
- if (hasOwnProperty.call(object, key)) {
- return false;
- }
- }
- return true;
- },
-
- template: `
- <tr>
- <td>
- <a v-if="!model.isFolder"
- class="environment-name"
- :href="environmentPath">
- {{model.name}}
- </a>
- <a v-else class="folder-name" :href="folderUrl">
- <span class="folder-icon">
- <i class="fa fa-folder" aria-hidden="true"></i>
- </span>
-
- <span>
- {{model.folderName}}
- </span>
-
- <span class="badge">
- {{model.size}}
- </span>
- </a>
- </td>
-
- <td class="deployment-column">
- <span v-if="shouldRenderDeploymentID">
- {{deploymentInternalId}}
- </span>
-
- <span v-if="!model.isFolder && deploymentHasUser">
- by
- <a :href="deploymentUser.web_url" class="js-deploy-user-container">
- <img class="avatar has-tooltip s20"
- :src="deploymentUser.avatar_url"
- :alt="userImageAltDescription"
- :title="deploymentUser.username" />
- </a>
- </span>
- </td>
-
- <td class="environments-build-cell">
- <a v-if="shouldRenderBuildName"
- class="build-link"
- :href="buildPath">
- {{buildName}}
- </a>
- </td>
-
- <td>
- <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"
- :commit-icon-svg="commitIconSvg">
- </commit-component>
- </div>
- <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
- No deployments yet
- </p>
- </td>
-
- <td>
- <span v-if="!model.isFolder && canShowDate"
- class="environment-created-date-timeago">
- {{createdDate}}
- </span>
- </td>
-
- <td class="hidden-xs">
- <div v-if="!model.isFolder">
- <div v-if="hasManualActions && canCreateDeployment"
- class="inline js-manual-actions-container">
- <actions-component
- :play-icon-svg="playIconSvg"
- :actions="manualActions">
- </actions-component>
- </div>
-
- <div v-if="externalURL && canReadEnvironment"
- class="inline js-external-url-container">
- <external-url-component
- :external-url="externalURL">
- </external-url-component>
- </div>
-
- <div v-if="hasStopAction && canCreateDeployment"
- class="inline js-stop-component-container">
- <stop-component
- :stop-url="model.stop_path">
- </stop-component>
- </div>
-
- <div v-if="model && model.terminal_path"
- class="inline js-terminal-button-container">
- <terminal-button-component
- :terminal-icon-svg="terminalIconSvg"
- :terminal-path="model.terminal_path">
- </terminal-button-component>
- </div>
-
- <div v-if="canRetry && canCreateDeployment"
- class="inline js-rollback-component-container">
- <rollback-component
- :is-last-deployment="isLastDeployment"
- :retry-url="retryUrl">
- </rollback-component>
- </div>
- </div>
- </td>
- </tr>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js
new file mode 100644
index 00000000000..baa15d9e5b5
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_rollback.js
@@ -0,0 +1,67 @@
+/* global Flash */
+/* eslint-disable no-new */
+/**
+ * Renders Rollback or Re deploy button in environments table depending
+ * of the provided property `isLastDeployment`.
+ *
+ * Makes a post request when the button is clicked.
+ */
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ retryUrl: {
+ type: String,
+ default: '',
+ },
+
+ isLastDeployment: {
+ type: Boolean,
+ default: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ this.service.postAction(this.retryUrl)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button type="button"
+ class="btn"
+ @click="onClick"
+ :disabled="isLoading">
+
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
+
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
deleted file mode 100644
index daf126eb4e8..00000000000
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Renders Rollback or Re deploy button in environments table depending
- * of the provided property `isLastDeployment`
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('rollback-component', {
- props: {
- retryUrl: {
- type: String,
- default: '',
- },
-
- isLastDeployment: {
- type: Boolean,
- default: true,
- },
- },
-
- template: `
- <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
- <span v-if="isLastDeployment">
- Re-deploy
- </span>
- <span v-else>
- Rollback
- </span>
- </a>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
new file mode 100644
index 00000000000..5404d647745
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -0,0 +1,56 @@
+/* global Flash */
+/* eslint-disable no-new, no-alert */
+/**
+ * Renders the stop "button" that allows stop an environment.
+ * Used in environments table.
+ */
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ stopUrl: {
+ type: String,
+ default: '',
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClick() {
+ if (confirm('Are you sure you want to stop this environment?')) {
+ this.isLoading = true;
+
+ this.service.postAction(this.retryUrl)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshEnvironments');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.', 'alert');
+ });
+ }
+ },
+ },
+
+ template: `
+ <button type="button"
+ class="btn stop-env-link"
+ @click="onClick"
+ :disabled="isLoading"
+ title="Stop Environment">
+ <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
deleted file mode 100644
index 96983a19568..00000000000
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Renders the stop "button" that allows stop an environment.
- * Used in environments table.
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('stop-component', {
- props: {
- stopUrl: {
- type: String,
- default: '',
- },
- },
-
- template: `
- <a class="btn stop-env-link"
- :href="stopUrl"
- data-confirm="Are you sure you want to stop this environment?"
- data-method="post"
- rel="nofollow">
- <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
- </a>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js
new file mode 100644
index 00000000000..66a71faa02f
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -0,0 +1,27 @@
+/**
+ * Renders a terminal button to open a web terminal.
+ * Used in environments table.
+ */
+import terminalIconSvg from 'icons/_icon_terminal.svg';
+
+export default {
+ props: {
+ terminalPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return { terminalIconSvg };
+ },
+
+ template: `
+ <a class="btn terminal-button"
+ title="Open web terminal"
+ :href="terminalPath">
+ ${terminalIconSvg}
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
deleted file mode 100644
index 481e0d15e7a..00000000000
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Renders a terminal button to open a web terminal.
- * Used in environments table.
- */
-const Vue = require('vue');
-
-module.exports = Vue.component('terminal-button-component', {
- props: {
- terminalPath: {
- type: String,
- default: '',
- },
- terminalIconSvg: {
- type: String,
- default: '',
- },
- },
-
- template: `
- <a class="btn terminal-button"
- :href="terminalPath">
- <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
- </a>
- `,
-});
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
new file mode 100644
index 00000000000..338dff40bc9
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -0,0 +1,60 @@
+/**
+ * Render environments table.
+ */
+import EnvironmentTableRowComponent from './environment_item';
+
+export default {
+ components: {
+ 'environment-item': EnvironmentTableRowComponent,
+ },
+
+ props: {
+ environments: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="environments-name">Environment</th>
+ <th class="environments-deploy">Last deployment</th>
+ <th class="environments-build">Job</th>
+ <th class="environments-commit">Commit</th>
+ <th class="environments-date">Updated</th>
+ <th class="environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in environments"
+ v-bind:model="model">
+ <tr is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6
deleted file mode 100644
index fd35d77fd3d..00000000000
--- a/app/assets/javascripts/environments/components/environments_table.js.es6
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Render environments table.
- */
-const Vue = require('vue');
-const EnvironmentItem = require('./environment_item');
-
-module.exports = Vue.component('environment-table-component', {
-
- components: {
- 'environment-item': EnvironmentItem,
- },
-
- props: {
- environments: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- commitIconSvg: {
- type: String,
- required: false,
- },
-
- playIconSvg: {
- type: String,
- required: false,
- },
-
- terminalIconSvg: {
- type: String,
- required: false,
- },
- },
-
- template: `
- <table class="table ci-table environments">
- <thead>
- <tr>
- <th class="environments-name">Environment</th>
- <th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Job</th>
- <th class="environments-commit">Commit</th>
- <th class="environments-date">Updated</th>
- <th class="hidden-xs environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in environments"
- v-bind:model="model">
- <tr is="environment-item"
- :model="model"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg"></tr>
- </template>
- </tbody>
- </table>
- `,
-});
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
new file mode 100644
index 00000000000..8d963b335cf
--- /dev/null
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -0,0 +1,13 @@
+import EnvironmentsComponent from './components/environment';
+
+$(() => {
+ window.gl = window.gl || {};
+
+ if (gl.EnvironmentsListApp) {
+ gl.EnvironmentsListApp.$destroy(true);
+ }
+
+ gl.EnvironmentsListApp = new EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ });
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
deleted file mode 100644
index 7bbba91bc10..00000000000
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-const EnvironmentsComponent = require('./components/environment');
-
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListApp) {
- gl.EnvironmentsListApp.$destroy(true);
- }
-
- gl.EnvironmentsListApp = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-});
diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/environments/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
new file mode 100644
index 00000000000..f939eccf246
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -0,0 +1,13 @@
+import EnvironmentsFolderComponent from './environments_folder_view';
+
+$(() => {
+ window.gl = window.gl || {};
+
+ if (gl.EnvironmentsListFolderApp) {
+ gl.EnvironmentsListFolderApp.$destroy(true);
+ }
+
+ gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
deleted file mode 100644
index d2ca465351a..00000000000
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-const EnvironmentsFolderComponent = require('./environments_folder_view');
-
-$(() => {
- window.gl = window.gl || {};
-
- if (gl.EnvironmentsListFolderApp) {
- gl.EnvironmentsListFolderApp.$destroy(true);
- }
-
- gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
-});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
new file mode 100644
index 00000000000..8abbcf0c227
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -0,0 +1,178 @@
+/* eslint-disable no-new */
+/* global Flash */
+import Vue from 'vue';
+import EnvironmentsService from '../services/environments_service';
+import EnvironmentTable from '../components/environments_table';
+import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
+
+export default Vue.component('environment-folder-view', {
+ components: {
+ 'environment-table': EnvironmentTable,
+ 'table-pagination': TablePaginationComponent,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
+ const store = new EnvironmentsStore();
+ const pathname = window.location.pathname;
+ const endpoint = `${pathname}.json`;
+ const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
+
+ return {
+ store,
+ folderName,
+ endpoint,
+ state: store.state,
+ visibility: 'available',
+ isLoading: false,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateDeployment: environmentsData.canCreateDeployment,
+ canReadEnvironment: environmentsData.canReadEnvironment,
+
+ // svgs
+ commitIconSvg: environmentsData.commitIconSvg,
+ playIconSvg: environmentsData.playIconSvg,
+ terminalIconSvg: environmentsData.terminalIconSvg,
+
+ // Pagination Properties,
+ paginationInformation: {},
+ pageNumber: 1,
+ };
+ },
+
+ computed: {
+ scope() {
+ return gl.utils.getParameterByName('scope');
+ },
+
+ canReadEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
+ },
+
+ canCreateDeploymentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
+ },
+
+ /**
+ * URL to link in the stopped tab.
+ *
+ * @return {String}
+ */
+ stoppedPath() {
+ return `${window.location.pathname}?scope=stopped`;
+ },
+
+ /**
+ * URL to link in the available tab.
+ *
+ * @return {String}
+ */
+ availablePath() {
+ return window.location.pathname;
+ },
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
+
+ this.service = new EnvironmentsService(endpoint);
+
+ this.isLoading = true;
+
+ return this.service.get()
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.', 'alert');
+ });
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ changePage(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
+ },
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area" v-if="!isLoading">
+
+ <h4 class="js-folder-name environments-folder-name">
+ Environments / <b>{{folderName}}</b>
+ </h4>
+
+ <ul class="nav-links">
+ <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
+ <a :href="availablePath" class="js-available-environments-folder-tab">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="stoppedPath" class="js-stopped-environments-folder-tab">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg"
+ :service="service"/>
+
+ <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation"/>
+ </div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
deleted file mode 100644
index 53d52965758..00000000000
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
+++ /dev/null
@@ -1,182 +0,0 @@
-/* eslint-disable no-param-reassign, no-new */
-/* global Flash */
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-const EnvironmentsService = require('../services/environments_service');
-const EnvironmentTable = require('../components/environments_table');
-const EnvironmentsStore = require('../stores/environments_store');
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
-module.exports = Vue.component('environment-folder-view', {
-
- components: {
- 'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
- },
-
- data() {
- const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
- const store = new EnvironmentsStore();
- const pathname = window.location.pathname;
- const endpoint = `${pathname}.json`;
- const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
-
- return {
- store,
- folderName,
- endpoint,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
-
- // svgs
- commitIconSvg: environmentsData.commitIconSvg,
- playIconSvg: environmentsData.playIconSvg,
- terminalIconSvg: environmentsData.terminalIconSvg,
-
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return gl.utils.getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- /**
- * URL to link in the stopped tab.
- *
- * @return {String}
- */
- stoppedPath() {
- return `${window.location.pathname}?scope=stopped`;
- },
-
- /**
- * URL to link in the available tab.
- *
- * @return {String}
- */
- availablePath() {
- return window.location.pathname;
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- const service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return service.all()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
- },
-
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- changePage(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
- },
-
- template: `
- <div :class="cssContainerClass">
- <div class="top-area" v-if="!isLoading">
-
- <h4 class="js-folder-name environments-folder-name">
- Environments / <b>{{folderName}}</b>
- </h4>
-
- <ul class="nav-links">
- <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
- <a :href="availablePath" class="js-available-environments-folder-tab">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="stoppedPath" class="js-stopped-environments-folder-tab">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg">
- </environment-table>
-
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
- </div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
new file mode 100644
index 00000000000..07040bf0d73
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -0,0 +1,19 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class EnvironmentsService {
+ constructor(endpoint) {
+ this.environments = Vue.resource(endpoint);
+ }
+
+ get(scope, page) {
+ return this.environments.get({ scope, page });
+ }
+
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
deleted file mode 100644
index 9cef335868e..00000000000
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ /dev/null
@@ -1,13 +0,0 @@
-const Vue = require('vue');
-
-class EnvironmentsService {
- constructor(endpoint) {
- this.environments = Vue.resource(endpoint);
- }
-
- all() {
- return this.environments.get();
- }
-}
-
-module.exports = EnvironmentsService;
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
new file mode 100644
index 00000000000..3c3084f3b78
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -0,0 +1,88 @@
+import '~/lib/utils/common_utils';
+/**
+ * Environments Store.
+ *
+ * Stores received environments, count of stopped environments and count of
+ * available environments.
+ */
+export default class EnvironmentsStore {
+ constructor() {
+ this.state = {};
+ this.state.environments = [];
+ this.state.stoppedCounter = 0;
+ this.state.availableCounter = 0;
+ this.state.paginationInformation = {};
+
+ return this;
+ }
+
+ /**
+ *
+ * Stores the received environments.
+ *
+ * In the main environments endpoint, each environment has the following schema
+ * { name: String, size: Number, latest: Object }
+ * In the endpoint to retrieve environments from each folder, the environment does
+ * not have the `latest` key and the data is all in the root level.
+ * To avoid doing this check in the view, we store both cases the same by extracting
+ * what is inside the `latest` key.
+ *
+ * If the `size` is bigger than 1, it means it should be rendered as a folder.
+ * In those cases we add `isFolder` key in order to render it properly.
+ *
+ * @param {Array} environments
+ * @returns {Array}
+ */
+ storeEnvironments(environments = []) {
+ const filteredEnvironments = environments.map((env) => {
+ let filtered = {};
+
+ if (env.size > 1) {
+ filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
+ }
+
+ if (env.latest) {
+ filtered = Object.assign(filtered, env, env.latest);
+ delete filtered.latest;
+ } else {
+ filtered = Object.assign(filtered, env);
+ }
+
+ return filtered;
+ });
+
+ this.state.environments = filteredEnvironments;
+
+ return filteredEnvironments;
+ }
+
+ setPagination(pagination = {}) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
+
+ this.state.paginationInformation = paginationInformation;
+ return paginationInformation;
+ }
+
+ /**
+ * Stores the number of available environments.
+ *
+ * @param {Number} count = 0
+ * @return {Number}
+ */
+ storeAvailableCount(count = 0) {
+ this.state.availableCounter = count;
+ return count;
+ }
+
+ /**
+ * Stores the number of closed environments.
+ *
+ * @param {Number} count = 0
+ * @return {Number}
+ */
+ storeStoppedCount(count = 0) {
+ this.state.stoppedCounter = count;
+ return count;
+ }
+}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
deleted file mode 100644
index 15cd9bde08e..00000000000
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ /dev/null
@@ -1,90 +0,0 @@
-require('~/lib/utils/common_utils');
-/**
- * Environments Store.
- *
- * Stores received environments, count of stopped environments and count of
- * available environments.
- */
-class EnvironmentsStore {
- constructor() {
- this.state = {};
- this.state.environments = [];
- this.state.stoppedCounter = 0;
- this.state.availableCounter = 0;
- this.state.paginationInformation = {};
-
- return this;
- }
-
- /**
- *
- * Stores the received environments.
- *
- * In the main environments endpoint, each environment has the following schema
- * { name: String, size: Number, latest: Object }
- * In the endpoint to retrieve environments from each folder, the environment does
- * not have the `latest` key and the data is all in the root level.
- * To avoid doing this check in the view, we store both cases the same by extracting
- * what is inside the `latest` key.
- *
- * If the `size` is bigger than 1, it means it should be rendered as a folder.
- * In those cases we add `isFolder` key in order to render it properly.
- *
- * @param {Array} environments
- * @returns {Array}
- */
- storeEnvironments(environments = []) {
- const filteredEnvironments = environments.map((env) => {
- let filtered = {};
-
- if (env.size > 1) {
- filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
- }
-
- if (env.latest) {
- filtered = Object.assign(filtered, env, env.latest);
- delete filtered.latest;
- } else {
- filtered = Object.assign(filtered, env);
- }
-
- return filtered;
- });
-
- this.state.environments = filteredEnvironments;
-
- return filteredEnvironments;
- }
-
- setPagination(pagination = {}) {
- const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
- const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
-
- this.state.paginationInformation = paginationInformation;
- return paginationInformation;
- }
-
- /**
- * Stores the number of available environments.
- *
- * @param {Number} count = 0
- * @return {Number}
- */
- storeAvailableCount(count = 0) {
- this.state.availableCounter = count;
- return count;
- }
-
- /**
- * Stores the number of closed environments.
- *
- * @param {Number} count = 0
- * @return {Number}
- */
- storeStoppedCount(count = 0) {
- this.state.stoppedCounter = count;
- return count;
- }
-}
-
-module.exports = EnvironmentsStore;
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
new file mode 100644
index 00000000000..027222f804d
--- /dev/null
+++ b/app/assets/javascripts/extensions/array.js
@@ -0,0 +1,11 @@
+// TODO: remove this
+
+// eslint-disable-next-line no-extend-native
+Array.prototype.first = function first() {
+ return this[0];
+};
+
+// eslint-disable-next-line no-extend-native
+Array.prototype.last = function last() {
+ return this[this.length - 1];
+};
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
deleted file mode 100644
index f8256a8d26d..00000000000
--- a/app/assets/javascripts/extensions/array.js.es6
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */
-
-'use strict';
-
-Array.prototype.first = function() {
- return this[0];
-};
-
-Array.prototype.last = function() {
- return this[this.length-1];
-};
-
-Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
- if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
- if (typeof predicate !== 'function') throw new TypeError('predicate must be a function');
-
- const list = Object(this);
- const thisArg = args[1];
- let value = {};
-
- for (let i = 0; i < list.length; i += 1) {
- value = list[i];
- if (predicate.call(thisArg, value, i, list)) return value;
- }
-
- return undefined;
-};
diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6
deleted file mode 100644
index abedae4c1c7..00000000000
--- a/app/assets/javascripts/extensions/custom_event.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* global CustomEvent */
-/* eslint-disable no-global-assign */
-
-// Custom event support for IE
-CustomEvent = function CustomEvent(event, parameters) {
- const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
- const evt = document.createEvent('CustomEvent');
- evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
- return evt;
-};
-
-CustomEvent.prototype = window.Event.prototype;
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
deleted file mode 100644
index 90ab79305a7..00000000000
--- a/app/assets/javascripts/extensions/element.js.es6
+++ /dev/null
@@ -1,20 +0,0 @@
-/* global Element */
-/* eslint-disable consistent-return, max-len, no-empty, func-names */
-
-Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
- if (!selectedElement) return;
- return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
-};
-
-Element.prototype.matches = Element.prototype.matches ||
- Element.prototype.matchesSelector ||
- Element.prototype.mozMatchesSelector ||
- Element.prototype.msMatchesSelector ||
- Element.prototype.oMatchesSelector ||
- Element.prototype.webkitMatchesSelector ||
- function (s) {
- const matches = (this.document || this.ownerDocument).querySelectorAll(s);
- let i = matches.length - 1;
- while (i >= 0 && matches.item(i) !== this) { i -= 1; }
- return i > -1;
- };
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
deleted file mode 100644
index 1a489b859e8..00000000000
--- a/app/assets/javascripts/extensions/jquery.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
-// Disable an element and add the 'disabled' Bootstrap class
-(function() {
- $.fn.extend({
- disable: function() {
- return $(this).attr('disabled', 'disabled').addClass('disabled');
- }
- });
-
- // Enable an element and remove the 'disabled' Bootstrap class
- $.fn.extend({
- enable: function() {
- return $(this).removeAttr('disabled').removeClass('disabled');
- }
- });
-}).call(window);
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
deleted file mode 100644
index 70a2d765abd..00000000000
--- a/app/assets/javascripts/extensions/object.js.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-/* eslint-disable no-restricted-syntax */
-
-// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
-if (typeof Object.assign !== 'function') {
- Object.assign = function assign(target, ...args) {
- if (target == null) { // TypeError if undefined or null
- throw new TypeError('Cannot convert undefined or null to object');
- }
-
- const to = Object(target);
-
- for (let index = 0; index < args.length; index += 1) {
- const nextSource = args[index];
-
- if (nextSource != null) { // Skip over if undefined or null
- for (const nextKey in nextSource) {
- // Avoid bugs when hasOwnProperty is shadowed
- if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
- to[nextKey] = nextSource[nextKey];
- }
- }
- }
- }
- return to;
- };
-}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 698870d0ce1..3f041172ff3 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,147 +1,141 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
+/* global notes */
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+let $commentButtonTemplate;
+var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- this.FilesCommentButton = (function() {
- var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
+window.FilesCommentButton = (function() {
+ var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
- COMMENT_BUTTON_CLASS = '.add-diff-note';
+ COMMENT_BUTTON_CLASS = '.add-diff-note';
- COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+ LINE_HOLDER_CLASS = '.line_holder';
- LINE_HOLDER_CLASS = '.line_holder';
+ LINE_NUMBER_CLASS = 'diff-line-num';
- LINE_NUMBER_CLASS = 'diff-line-num';
+ LINE_CONTENT_CLASS = 'line_content';
- LINE_CONTENT_CLASS = 'line_content';
+ UNFOLDABLE_LINE_CLASS = 'js-unfold';
- UNFOLDABLE_LINE_CLASS = 'js-unfold';
+ EMPTY_CELL_CLASS = 'empty-cell';
- EMPTY_CELL_CLASS = 'empty-cell';
+ OLD_LINE_CLASS = 'old_line';
- OLD_LINE_CLASS = 'old_line';
+ LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
- LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
+ TEXT_FILE_SELECTOR = '.text-file';
- TEXT_FILE_SELECTOR = '.text-file';
+ function FilesCommentButton(filesContainerElement) {
+ this.render = bind(this.render, this);
+ this.hideButton = bind(this.hideButton, this);
+ this.isParallelView = notes.isParallelView();
+ filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
+ .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
+ }
- DEBOUNCE_TIMEOUT_DURATION = 100;
+ FilesCommentButton.prototype.render = function(e) {
+ var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
+ $currentTarget = $(e.currentTarget);
- function FilesCommentButton(filesContainerElement) {
- var debounce;
- this.filesContainerElement = filesContainerElement;
- this.destroy = bind(this.destroy, this);
- this.render = bind(this.render, this);
- this.VIEW_TYPE = $('input#view[type=hidden]').val();
- debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
- $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+ if ($currentTarget.hasClass('js-no-comment-btn')) return;
+
+ lineContentElement = this.getLineContent($currentTarget);
+ buttonParentElement = this.getButtonParent($currentTarget);
+
+ if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
+
+ $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
+ buttonParentElement.addClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
+
+ if ($button.length) {
+ return;
}
- FilesCommentButton.prototype.render = function(e) {
- var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
- $currentTarget = $(e.currentTarget);
-
- buttonParentElement = this.getButtonParent($currentTarget);
- if (!this.validateButtonParent(buttonParentElement)) return;
- lineContentElement = this.getLineContent($currentTarget);
- if (!this.validateLineContent(lineContentElement)) return;
-
- textFileElement = this.getTextFileElement($currentTarget);
- buttonParentElement.append(this.buildButton({
- noteableType: textFileElement.attr('data-noteable-type'),
- noteableID: textFileElement.attr('data-noteable-id'),
- commitID: textFileElement.attr('data-commit-id'),
- noteType: lineContentElement.attr('data-note-type'),
- position: lineContentElement.attr('data-position'),
- lineType: lineContentElement.attr('data-line-type'),
- discussionID: lineContentElement.attr('data-discussion-id'),
- lineCode: lineContentElement.attr('data-line-code')
- }));
- };
-
- FilesCommentButton.prototype.destroy = function(e) {
- if (this.isMovingToSameType(e)) {
- return;
- }
- $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove();
- };
-
- FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
- var initializedButtonTemplate;
- initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
- COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
- });
- return $(initializedButtonTemplate).attr({
- 'data-noteable-type': buttonAttributes.noteableType,
- 'data-noteable-id': buttonAttributes.noteableID,
- 'data-commit-id': buttonAttributes.commitID,
- 'data-note-type': buttonAttributes.noteType,
- 'data-line-code': buttonAttributes.lineCode,
- 'data-position': buttonAttributes.position,
- 'data-discussion-id': buttonAttributes.discussionID,
- 'data-line-type': buttonAttributes.lineType
- });
- };
-
- FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
- return $(hoveredElement.closest(TEXT_FILE_SELECTOR));
- };
-
- FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
- if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+ textFileElement = this.getTextFileElement($currentTarget);
+ buttonParentElement.append(this.buildButton({
+ noteableType: textFileElement.attr('data-noteable-type'),
+ noteableID: textFileElement.attr('data-noteable-id'),
+ commitID: textFileElement.attr('data-commit-id'),
+ noteType: lineContentElement.attr('data-note-type'),
+ position: lineContentElement.attr('data-position'),
+ lineType: lineContentElement.attr('data-line-type'),
+ discussionID: lineContentElement.attr('data-discussion-id'),
+ lineCode: lineContentElement.attr('data-line-code')
+ }));
+ };
+
+ FilesCommentButton.prototype.hideButton = function(e) {
+ var $currentTarget = $(e.currentTarget);
+ var buttonParentElement = this.getButtonParent($currentTarget);
+
+ buttonParentElement.removeClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
+ };
+
+ FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
+ return $commentButtonTemplate.clone().attr({
+ 'data-noteable-type': buttonAttributes.noteableType,
+ 'data-noteable-id': buttonAttributes.noteableID,
+ 'data-commit-id': buttonAttributes.commitID,
+ 'data-note-type': buttonAttributes.noteType,
+ 'data-line-code': buttonAttributes.lineCode,
+ 'data-position': buttonAttributes.position,
+ 'data-discussion-id': buttonAttributes.discussionID,
+ 'data-line-type': buttonAttributes.lineType
+ });
+ };
+
+ FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
+ return hoveredElement.closest(TEXT_FILE_SELECTOR);
+ };
+
+ FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
+ if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+ return hoveredElement;
+ }
+ if (!this.isParallelView) {
+ return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
+ } else {
+ return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
+ }
+ };
+
+ FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
+ if (!this.isParallelView) {
+ if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
}
- if (this.VIEW_TYPE === 'inline') {
- return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
- } else {
- return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
- }
- };
-
- FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
- if (this.VIEW_TYPE === 'inline') {
- if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
- return hoveredElement;
- }
- return hoveredElement.parent().find("." + OLD_LINE_CLASS);
- } else {
- if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
- return hoveredElement;
- }
- return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ return hoveredElement.parent().find("." + OLD_LINE_CLASS);
+ } else {
+ if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
+ return hoveredElement;
}
- };
+ return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ }
+ };
- FilesCommentButton.prototype.isMovingToSameType = function(e) {
- var newButtonParent;
- newButtonParent = this.getButtonParent($(e.toElement));
- if (!newButtonParent) {
- return false;
- }
- return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
- };
+ FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
+ return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
+ };
- FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
- return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0;
- };
+ FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
+ return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+ };
- FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
- return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
- };
+ return FilesCommentButton;
+})();
- return FilesCommentButton;
- })();
+$.fn.filesCommentButton = function() {
+ $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
- $.fn.filesCommentButton = function() {
- if (!(this && (this.parent().data('can-create-note') != null))) {
- return;
+ if (!(this && (this.parent().data('can-create-note') != null))) {
+ return;
+ }
+ return this.each(function() {
+ if (!$.data(this, 'filesCommentButton')) {
+ return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
}
- return this.each(function() {
- if (!$.data(this, 'filesCommentButton')) {
- return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
- }
- });
- };
-}).call(window);
+ });
+};
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
new file mode 100644
index 00000000000..aaaeb9bddb1
--- /dev/null
+++ b/app/assets/javascripts/filterable_list.js
@@ -0,0 +1,46 @@
+/**
+ * Makes search request for content when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+
+export default class FilterableList {
+ constructor(form, filter, holder) {
+ this.filterForm = form;
+ this.listFilterElement = filter;
+ this.listHolderElement = holder;
+ }
+
+ initSearch() {
+ this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
+
+ this.listFilterElement.removeEventListener('input', this.debounceFilter);
+ this.listFilterElement.addEventListener('input', this.debounceFilter);
+ }
+
+ filterResults() {
+ const form = this.filterForm;
+ const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
+
+ $(this.listHolderElement).fadeTo(250, 0.5);
+
+ return $.ajax({
+ url: form.getAttribute('action'),
+ data: $(form).serialize(),
+ type: 'GET',
+ dataType: 'json',
+ context: this,
+ complete() {
+ $(this.listHolderElement).fadeTo(250, 1);
+ },
+ success(data) {
+ this.listHolderElement.innerHTML = data.html;
+
+ // Change url so if user reload a page - search results are saved
+ return window.history.replaceState({
+ page: filterUrl,
+
+ }, document.title, filterUrl);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/filtered_search/container.js b/app/assets/javascripts/filtered_search/container.js
new file mode 100644
index 00000000000..2243c4dd2c5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/container.js
@@ -0,0 +1,14 @@
+/* eslint-disable class-methods-use-this */
+let container = document;
+
+class FilteredSearchContainerClass {
+ set container(containerParam) {
+ container = containerParam;
+ }
+
+ get container() {
+ return container;
+ }
+}
+
+export default new FilteredSearchContainerClass();
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
new file mode 100644
index 00000000000..98dcb697af9
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -0,0 +1,83 @@
+require('./filtered_search_dropdown');
+
+/* global droplabFilter */
+
+(() => {
+ class DropdownHint extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabFilter: {
+ template: 'hint',
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI') {
+ if (selected.hasAttribute('data-value')) {
+ this.dismissDropdown();
+ } else if (selected.getAttribute('data-action') === 'submit') {
+ this.dismissDropdown();
+ this.dispatchFormSubmitEvent();
+ } else {
+ const token = selected.querySelector('.js-filter-hint').innerText.trim();
+ const tag = selected.querySelector('.js-filter-tag').innerText.trim();
+
+ if (tag.length) {
+ // Get previous input values in the input field and convert them into visual tokens
+ const previousInputValues = this.input.value.split(' ');
+ const searchTerms = [];
+
+ previousInputValues.forEach((value, index) => {
+ searchTerms.push(value);
+
+ if (index === previousInputValues.length - 1
+ && token.indexOf(value.toLowerCase()) !== -1) {
+ searchTerms.pop();
+ }
+ });
+
+ if (searchTerms.length > 0) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+ }
+
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
+ }
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+ }
+
+ renderContent() {
+ const dropdownData = [];
+
+ [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ const { icon, hint, tag, type } = dropdownMenu.dataset;
+ if (icon && hint && tag) {
+ dropdownData.push(
+ Object.assign({
+ icon: `fa-${icon}`,
+ hint,
+ tag: `&lt;${tag}&gt;`,
+ }, type && { type }),
+ );
+ }
+ });
+
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownHint = DropdownHint;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
deleted file mode 100644
index 9e92d544bef..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ /dev/null
@@ -1,64 +0,0 @@
-require('./filtered_search_dropdown');
-
-/* global droplabFilter */
-
-(() => {
- class DropdownHint extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- droplabFilter: {
- template: 'hint',
- filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
- },
- };
- }
-
- itemClicked(e) {
- const { selected } = e.detail;
-
- if (selected.tagName === 'LI') {
- if (selected.hasAttribute('data-value')) {
- this.dismissDropdown();
- } else if (selected.getAttribute('data-action') === 'submit') {
- this.dismissDropdown();
- this.dispatchFormSubmitEvent();
- } else {
- const token = selected.querySelector('.js-filter-hint').innerText.trim();
- const tag = selected.querySelector('.js-filter-tag').innerText.trim();
-
- if (tag.length) {
- gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
- }
- this.dismissDropdown();
- this.dispatchInputEvent();
- }
- }
- }
-
- renderContent() {
- const dropdownData = [];
-
- [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
- const { icon, hint, tag } = dropdownMenu.dataset;
- if (icon && hint && tag) {
- dropdownData.push({
- icon: `fa-${icon}`,
- hint,
- tag: `&lt;${tag}&gt;`,
- });
- }
- });
-
- this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
- this.droplab.setData(this.hookId, dropdownData);
- }
-
- init() {
- this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
- }
- }
-
- window.gl = window.gl || {};
- gl.DropdownHint = DropdownHint;
-})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index b3dc3e502c5..b3dc3e502c5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
new file mode 100644
index 00000000000..04e2afad02f
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -0,0 +1,65 @@
+require('./filtered_search_dropdown');
+
+/* global droplabAjaxFilter */
+
+(() => {
+ class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabAjaxFilter: {
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
+ searchKey: 'search',
+ params: {
+ per_page: 20,
+ active: true,
+ project_id: this.getProjectId(),
+ current_user: true,
+ },
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e,
+ selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ getProjectId() {
+ return this.input.getAttribute('data-project-id');
+ }
+
+ getSearchInput() {
+ const query = gl.DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+
+ let value = lastToken || '';
+
+ if (value[0] === '@') {
+ value = value.slice(1);
+ }
+
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
+ }
+
+ return value;
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownUser = DropdownUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
deleted file mode 100644
index 7e9c6f74aa5..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ /dev/null
@@ -1,60 +0,0 @@
-require('./filtered_search_dropdown');
-
-/* global droplabAjaxFilter */
-
-(() => {
- class DropdownUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- super(droplab, dropdown, input, filter);
- this.config = {
- droplabAjaxFilter: {
- endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
- searchKey: 'search',
- params: {
- per_page: 20,
- active: true,
- project_id: this.getProjectId(),
- current_user: true,
- },
- searchValueFunction: this.getSearchInput.bind(this),
- loadingTemplate: this.loadingTemplate,
- },
- };
- }
-
- itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
- }
-
- renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
- super.renderContent(forceShowList);
- }
-
- getProjectId() {
- return this.input.getAttribute('data-project-id');
- }
-
- getSearchInput() {
- const query = gl.DropdownUtils.getSearchInput(this.input);
- const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- let value = lastToken.value || '';
-
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if (value[0] === '"' || value[0] === '\'') {
- value = value.slice(1);
- }
-
- return value;
- }
-
- init() {
- this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
- }
- }
-
- window.gl = window.gl || {};
- gl.DropdownUser = DropdownUser;
-})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
new file mode 100644
index 00000000000..432b0c0dfd2
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -0,0 +1,181 @@
+import FilteredSearchContainer from './container';
+
+(() => {
+ class DropdownUtils {
+ static getEscapedText(text) {
+ let escapedText = text;
+ const hasSpace = text.indexOf(' ') !== -1;
+ const hasDoubleQuote = text.indexOf('"') !== -1;
+
+ // Encapsulate value with quotes if it has spaces
+ // Known side effect: values's with both single and double quotes
+ // won't escape properly
+ if (hasSpace) {
+ if (hasDoubleQuote) {
+ escapedText = `'${text}'`;
+ } else {
+ // Encapsulate singleQuotes or if it hasSpace
+ escapedText = `"${text}"`;
+ }
+ }
+
+ return escapedText;
+ }
+
+ static filterWithSymbol(filterSymbol, input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
+
+ const title = updatedItem.title.toLowerCase();
+ let value = searchInput.toLowerCase();
+ let symbol = '';
+
+ // Remove the symbol for filter
+ if (value[0] === filterSymbol) {
+ symbol = value[0];
+ value = value.slice(1);
+ }
+
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
+ }
+
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+
+ return updatedItem;
+ }
+
+ static filterHint(input, item) {
+ const updatedItem = item;
+ const searchInput = gl.DropdownUtils.getSearchQuery(input);
+ const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+ const lastKey = lastToken.key || lastToken || '';
+ const allowMultiple = item.type === 'array';
+ const itemInExistingTokens = tokens.some(t => t.key === item.hint);
+
+ if (!allowMultiple && itemInExistingTokens) {
+ updatedItem.droplab_hidden = true;
+ } else if (!lastKey || searchInput.split('').last() === ' ') {
+ updatedItem.droplab_hidden = false;
+ } else if (lastKey) {
+ const split = lastKey.split(':');
+ const tokenName = split[0].split(' ').last();
+
+ const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+ updatedItem.droplab_hidden = tokenName ? match : false;
+ }
+
+ return updatedItem;
+ }
+
+ static setDataValueIfSelected(filter, selected) {
+ const dataValue = selected.getAttribute('data-value');
+
+ if (dataValue) {
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
+ }
+
+ // Return boolean based on whether it was set
+ return dataValue !== null;
+ }
+
+ // Determines the full search query (visual tokens + input)
+ static getSearchQuery(untilInput = false) {
+ const container = FilteredSearchContainer.container;
+ const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
+ const values = [];
+
+ if (untilInput) {
+ const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
+ // Add one to include input-token to the tokens array
+ tokens.splice(inputIndex + 1);
+ }
+
+ tokens.forEach((token) => {
+ if (token.classList.contains('js-visual-token')) {
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+ const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
+ let valueText = '';
+
+ if (value && value.innerText) {
+ valueText = value.innerText;
+ }
+
+ if (token.className.indexOf('filtered-search-token') !== -1) {
+ values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
+ } else {
+ values.push(name.innerText);
+ }
+ } else if (token.classList.contains('input-token')) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const inputValue = input && input.value;
+
+ if (isLastVisualTokenValid) {
+ values.push(inputValue);
+ } else {
+ const previous = values.pop();
+ values.push(`${previous}${inputValue}`);
+ }
+ }
+ });
+
+ return values.join(' ');
+ }
+
+ static getSearchInput(filteredSearchInput) {
+ const inputValue = filteredSearchInput.value;
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+
+ return inputValue.slice(0, right);
+ }
+
+ static getInputSelectionPosition(input) {
+ const selectionStart = input.selectionStart;
+ let inputValue = input.value;
+ // Replace all spaces inside quote marks with underscores
+ // (will continue to match entire string until an end quote is found if any)
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+ // Get the right position for the word selected
+ // Regex matches first space
+ let right = inputValue.slice(selectionStart).search(/\s/);
+
+ if (right >= 0) {
+ right += selectionStart;
+ } else if (right < 0) {
+ right = inputValue.length;
+ }
+
+ // Get the left position for the word selected
+ // Regex matches last non-whitespace character
+ let left = inputValue.slice(0, right).search(/\S+$/);
+
+ if (selectionStart === 0) {
+ left = 0;
+ } else if (selectionStart === inputValue.length && left < 0) {
+ left = inputValue.length;
+ } else if (left < 0) {
+ left = selectionStart;
+ }
+
+ return {
+ left,
+ right,
+ };
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownUtils = DropdownUtils;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
deleted file mode 100644
index de3fa116717..00000000000
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ /dev/null
@@ -1,126 +0,0 @@
-(() => {
- class DropdownUtils {
- static getEscapedText(text) {
- let escapedText = text;
- const hasSpace = text.indexOf(' ') !== -1;
- const hasDoubleQuote = text.indexOf('"') !== -1;
-
- // Encapsulate value with quotes if it has spaces
- // Known side effect: values's with both single and double quotes
- // won't escape properly
- if (hasSpace) {
- if (hasDoubleQuote) {
- escapedText = `'${text}'`;
- } else {
- // Encapsulate singleQuotes or if it hasSpace
- escapedText = `"${text}"`;
- }
- }
-
- return escapedText;
- }
-
- static filterWithSymbol(filterSymbol, input, item) {
- const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
-
- if (lastToken !== searchToken) {
- const title = updatedItem.title.toLowerCase();
- let value = lastToken.value.toLowerCase();
-
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
-
- // Eg. filterSymbol = ~ for labels
- const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
- const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
-
- updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
- } else {
- updatedItem.droplab_hidden = false;
- }
-
- return updatedItem;
- }
-
- static filterHint(input, item) {
- const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- lastToken = lastToken.key || lastToken || '';
-
- if (!lastToken || query.split('').last() === ' ') {
- updatedItem.droplab_hidden = false;
- } else if (lastToken) {
- const split = lastToken.split(':');
- const tokenName = split[0].split(' ').last();
-
- const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
- updatedItem.droplab_hidden = tokenName ? match : false;
- }
-
- return updatedItem;
- }
-
- static setDataValueIfSelected(filter, selected) {
- const dataValue = selected.getAttribute('data-value');
-
- if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
- }
-
- // Return boolean based on whether it was set
- return dataValue !== null;
- }
-
- static getSearchInput(filteredSearchInput) {
- const inputValue = filteredSearchInput.value;
- const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
-
- return inputValue.slice(0, right);
- }
-
- static getInputSelectionPosition(input) {
- const selectionStart = input.selectionStart;
- let inputValue = input.value;
- // Replace all spaces inside quote marks with underscores
- // (will continue to match entire string until an end quote is found if any)
- // This helps with matching the beginning & end of a token:key
- inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
-
- // Get the right position for the word selected
- // Regex matches first space
- let right = inputValue.slice(selectionStart).search(/\s/);
-
- if (right >= 0) {
- right += selectionStart;
- } else if (right < 0) {
- right = inputValue.length;
- }
-
- // Get the left position for the word selected
- // Regex matches last non-whitespace character
- let left = inputValue.slice(0, right).search(/\S+$/);
-
- if (selectionStart === 0) {
- left = 0;
- } else if (selectionStart === inputValue.length && left < 0) {
- left = inputValue.length;
- } else if (left < 0) {
- left = selectionStart;
- }
-
- return {
- left,
- right,
- };
- }
- }
-
- window.gl = window.gl || {};
- gl.DropdownUtils = DropdownUtils;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index 392f1835966..856eb6590ee 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,3 +1,10 @@
-function requireAll(context) { return context.keys().map(context); }
-
-requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
+require('./dropdown_hint');
+require('./dropdown_non_user');
+require('./dropdown_user');
+require('./dropdown_utils');
+require('./filtered_search_dropdown_manager');
+require('./filtered_search_dropdown');
+require('./filtered_search_manager');
+require('./filtered_search_token_keys');
+require('./filtered_search_tokenizer');
+require('./filtered_search_visual_tokens');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
new file mode 100644
index 00000000000..e7bf530d343
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -0,0 +1,124 @@
+(() => {
+ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
+
+ class FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ this.droplab = droplab;
+ this.hookId = input && input.getAttribute('data-id');
+ this.input = input;
+ this.filter = filter;
+ this.dropdown = dropdown;
+ this.loadingTemplate = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ unbindEvents() {
+ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ getCurrentHook() {
+ return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
+ }
+
+ itemClicked(e, getValueFunction) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI' && selected.innerHTML) {
+ const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
+
+ if (!dataValueSet) {
+ const value = getValueFunction(selected);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
+ }
+
+ this.resetFilters();
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+
+ setAsDropdown() {
+ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ }
+
+ setOffset(offset = 0) {
+ if (window.innerWidth > 480) {
+ this.dropdown.style.left = `${offset}px`;
+ } else {
+ this.dropdown.style.left = '0px';
+ }
+ }
+
+ renderContent(forceShowList = false) {
+ const currentHook = this.getCurrentHook();
+ if (forceShowList && currentHook && currentHook.list.hidden) {
+ currentHook.list.show();
+ }
+ }
+
+ render(forceRenderContent = false, forceShowList = false) {
+ this.setAsDropdown();
+
+ const currentHook = this.getCurrentHook();
+ const firstTimeInitialized = currentHook === null;
+
+ if (firstTimeInitialized || forceRenderContent) {
+ this.renderContent(forceShowList);
+ } else if (currentHook.list.list.id !== this.dropdown.id) {
+ this.renderContent(forceShowList);
+ }
+ }
+
+ dismissDropdown() {
+ // Focusing on the input will dismiss dropdown
+ // (default droplab functionality)
+ this.input.focus();
+ }
+
+ dispatchInputEvent() {
+ // Propogate input change to FilteredSearchDropdownManager
+ // so that it can determine which dropdowns to open
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
+
+ dispatchFormSubmitEvent() {
+ // dispatchEvent() is necessary as form.submit() does not
+ // trigger event handlers
+ this.input.form.dispatchEvent(new Event('submit'));
+ }
+
+ hideDropdown() {
+ const currentHook = this.getCurrentHook();
+ if (currentHook) {
+ currentHook.list.hide();
+ }
+ }
+
+ resetFilters() {
+ const hook = this.getCurrentHook();
+
+ if (hook) {
+ const data = hook.list.data || [];
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdown = FilteredSearchDropdown;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
deleted file mode 100644
index fbc72a3001a..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ /dev/null
@@ -1,119 +0,0 @@
-(() => {
- const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
-
- class FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
- this.droplab = droplab;
- this.hookId = input && input.getAttribute('data-id');
- this.input = input;
- this.filter = filter;
- this.dropdown = dropdown;
- this.loadingTemplate = `<div class="filter-dropdown-loading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>`;
- this.bindEvents();
- }
-
- bindEvents() {
- this.itemClickedWrapper = this.itemClicked.bind(this);
- this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
- }
-
- unbindEvents() {
- this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
- }
-
- getCurrentHook() {
- return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
- }
-
- itemClicked(e, getValueFunction) {
- const { selected } = e.detail;
-
- if (selected.tagName === 'LI' && selected.innerHTML) {
- const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
-
- if (!dataValueSet) {
- const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
- }
-
- this.dismissDropdown();
- this.dispatchInputEvent();
- }
- }
-
- setAsDropdown() {
- this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
- }
-
- setOffset(offset = 0) {
- this.dropdown.style.left = `${offset}px`;
- }
-
- renderContent(forceShowList = false) {
- const currentHook = this.getCurrentHook();
- if (forceShowList && currentHook && currentHook.list.hidden) {
- currentHook.list.show();
- }
- }
-
- render(forceRenderContent = false, forceShowList = false) {
- this.setAsDropdown();
-
- const currentHook = this.getCurrentHook();
- const firstTimeInitialized = currentHook === null;
-
- if (firstTimeInitialized || forceRenderContent) {
- this.renderContent(forceShowList);
- } else if (currentHook.list.list.id !== this.dropdown.id) {
- this.renderContent(forceShowList);
- }
- }
-
- dismissDropdown() {
- // Focusing on the input will dismiss dropdown
- // (default droplab functionality)
- this.input.focus();
- }
-
- dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
- // so that it can determine which dropdowns to open
- this.input.dispatchEvent(new CustomEvent('input', {
- bubbles: true,
- cancelable: true,
- }));
- }
-
- dispatchFormSubmitEvent() {
- // dispatchEvent() is necessary as form.submit() does not
- // trigger event handlers
- this.input.form.dispatchEvent(new Event('submit'));
- }
-
- hideDropdown() {
- const currentHook = this.getCurrentHook();
- if (currentHook) {
- currentHook.list.hide();
- }
- }
-
- resetFilters() {
- const hook = this.getCurrentHook();
-
- if (hook) {
- const data = hook.list.data;
- const results = data.map((o) => {
- const updated = o;
- updated.droplab_hidden = false;
- return updated;
- });
- hook.list.render(results);
- }
- }
- }
-
- window.gl = window.gl || {};
- gl.FilteredSearchDropdown = FilteredSearchDropdown;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
new file mode 100644
index 00000000000..5fbe0450bb8
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -0,0 +1,191 @@
+/* global DropLab */
+import FilteredSearchContainer from './container';
+
+(() => {
+ class FilteredSearchDropdownManager {
+ constructor(baseEndpoint = '', page) {
+ this.container = FilteredSearchContainer.container;
+ this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.page = page;
+
+ this.setupMapping();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
+ }
+
+ this.setupMapping();
+
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ setupMapping() {
+ this.mapping = {
+ author: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-author'),
+ },
+ assignee: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: this.container.querySelector('#js-dropdown-assignee'),
+ },
+ milestone: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
+ element: this.container.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
+ element: this.container.querySelector('#js-dropdown-label'),
+ },
+ hint: {
+ reference: null,
+ gl: 'DropdownHint',
+ element: this.container.querySelector('#js-dropdown-hint'),
+ },
+ };
+ }
+
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ input.value = '';
+
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ }
+ }
+
+ updateCurrentDropdownOffset() {
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ updateDropdownOffset(key) {
+ // Always align dropdown with the input field
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+
+ const maxInputWidth = 240;
+ const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
+
+ // Make sure offset never exceeds the input container
+ const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ if (offsetMaxWidth < offset) {
+ offset = offsetMaxWidth;
+ }
+
+ this.mapping[key].reference.setOffset(offset);
+ }
+
+ load(key, firstLoad = false) {
+ const mappingKey = this.mapping[key];
+ const glClass = mappingKey.gl;
+ const element = mappingKey.element;
+ let forceShowList = false;
+
+ if (!mappingKey.reference) {
+ const dl = this.droplab;
+ const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
+ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+
+ // Passing glArguments to `new gl[glClass](<arguments>)`
+ mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
+ }
+
+ if (firstLoad) {
+ mappingKey.reference.init();
+ }
+
+ if (this.currentDropdown === 'hint') {
+ // Force the dropdown to show if it was clicked from the hint dropdown
+ forceShowList = true;
+ }
+
+ this.updateDropdownOffset(key);
+ mappingKey.reference.render(firstLoad, forceShowList);
+
+ this.currentDropdown = key;
+ }
+
+ loadDropdown(dropdownName = '') {
+ let firstLoad = false;
+
+ if (!this.droplab) {
+ firstLoad = true;
+ this.droplab = new DropLab();
+ }
+
+ const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
+ && this.mapping[match.key];
+ const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
+
+ if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
+ const key = match && match.key ? match.key : 'hint';
+ this.load(key, firstLoad);
+ }
+ }
+
+ setDropdown() {
+ const query = gl.DropdownUtils.getSearchQuery(true);
+ const { lastToken, searchToken } = this.tokenizer.processTokens(query);
+
+ if (this.currentDropdown) {
+ this.updateCurrentDropdownOffset();
+ }
+
+ if (lastToken === searchToken && lastToken !== null) {
+ // Token is not fully initialized yet because it has no value
+ // Eg. token = 'label:'
+
+ const split = lastToken.split(':');
+ const dropdownName = split[0].split(' ').last();
+ this.loadDropdown(split.length > 1 ? dropdownName : '');
+ } else if (lastToken) {
+ // Token has been initialized into an object because it has a value
+ this.loadDropdown(lastToken.key);
+ } else {
+ this.loadDropdown('hint');
+ }
+ }
+
+ resetDropdowns() {
+ if (!this.currentDropdown) {
+ return;
+ }
+
+ // Force current dropdown to hide
+ this.mapping[this.currentDropdown].reference.hideDropdown();
+
+ // Re-Load dropdown
+ this.setDropdown();
+
+ // Reset filters for current dropdown
+ this.mapping[this.currentDropdown].reference.resetFilters();
+
+ // Reposition dropdown so that it is aligned with cursor
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ destroyDroplab() {
+ this.droplab.destroy();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
deleted file mode 100644
index cecd3518ce3..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ /dev/null
@@ -1,210 +0,0 @@
-/* global DropLab */
-
-(() => {
- class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', page) {
- this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.filteredSearchInput = document.querySelector('.filtered-search');
- this.page = page;
-
- this.setupMapping();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
-
- cleanup() {
- if (this.droplab) {
- this.droplab.destroy();
- this.droplab = null;
- }
-
- this.setupMapping();
-
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
-
- setupMapping() {
- this.mapping = {
- author: {
- reference: null,
- gl: 'DropdownUser',
- element: document.querySelector('#js-dropdown-author'),
- },
- assignee: {
- reference: null,
- gl: 'DropdownUser',
- element: document.querySelector('#js-dropdown-assignee'),
- },
- milestone: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
- element: document.querySelector('#js-dropdown-milestone'),
- },
- label: {
- reference: null,
- gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
- element: document.querySelector('#js-dropdown-label'),
- },
- hint: {
- reference: null,
- gl: 'DropdownHint',
- element: document.querySelector('#js-dropdown-hint'),
- },
- };
- }
-
- static addWordToInput(tokenName, tokenValue = '') {
- const input = document.querySelector('.filtered-search');
- const inputValue = input.value;
- const word = `${tokenName}:${tokenValue}`;
-
- // Get the string to replace
- let newCaretPosition = input.selectionStart;
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
-
- input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
-
- // If we have added a tokenValue at the end of the input,
- // add a space and set selection to the end
- if (right >= inputValue.length && tokenValue !== '') {
- input.value += ' ';
- newCaretPosition = input.value.length;
- }
-
- gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
- }
-
- static updateInputCaretPosition(selectionStart, input) {
- // Reset the position
- // Sometimes can end up at end of input
- input.setSelectionRange(selectionStart, selectionStart);
-
- const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
-
- input.setSelectionRange(right, right);
- }
-
- updateCurrentDropdownOffset() {
- this.updateDropdownOffset(this.currentDropdown);
- }
-
- updateDropdownOffset(key) {
- if (!this.font) {
- this.font = window.getComputedStyle(this.filteredSearchInput).font;
- }
-
- const input = this.filteredSearchInput;
- const inputText = input.value.slice(0, input.selectionStart);
- const filterIconPadding = 27;
- let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
-
- const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
- this.mapping[key].element.clientWidth;
- const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
-
- if (offsetMaxWidth < offset) {
- offset = offsetMaxWidth;
- }
-
- this.mapping[key].reference.setOffset(offset);
- }
-
- load(key, firstLoad = false) {
- const mappingKey = this.mapping[key];
- const glClass = mappingKey.gl;
- const element = mappingKey.element;
- let forceShowList = false;
-
- if (!mappingKey.reference) {
- const dl = this.droplab;
- const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
- const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
-
- // Passing glArguments to `new gl[glClass](<arguments>)`
- mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
- }
-
- if (firstLoad) {
- mappingKey.reference.init();
- }
-
- if (this.currentDropdown === 'hint') {
- // Force the dropdown to show if it was clicked from the hint dropdown
- forceShowList = true;
- }
-
- this.updateDropdownOffset(key);
- mappingKey.reference.render(firstLoad, forceShowList);
-
- this.currentDropdown = key;
- }
-
- loadDropdown(dropdownName = '') {
- let firstLoad = false;
-
- if (!this.droplab) {
- firstLoad = true;
- this.droplab = new DropLab();
- }
-
- const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
- const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
-
- if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
- const key = match && match.key ? match.key : 'hint';
- this.load(key, firstLoad);
- }
- }
-
- setDropdown() {
- const { lastToken, searchToken } = this.tokenizer
- .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
-
- if (this.currentDropdown) {
- this.updateCurrentDropdownOffset();
- }
-
- if (lastToken === searchToken && lastToken !== null) {
- // Token is not fully initialized yet because it has no value
- // Eg. token = 'label:'
-
- const split = lastToken.split(':');
- const dropdownName = split[0].split(' ').last();
- this.loadDropdown(split.length > 1 ? dropdownName : '');
- } else if (lastToken) {
- // Token has been initialized into an object because it has a value
- this.loadDropdown(lastToken.key);
- } else {
- this.loadDropdown('hint');
- }
- }
-
- resetDropdowns() {
- // Force current dropdown to hide
- this.mapping[this.currentDropdown].reference.hideDropdown();
-
- // Re-Load dropdown
- this.setDropdown();
-
- // Reset filters for current dropdown
- this.mapping[this.currentDropdown].reference.resetFilters();
-
- // Reposition dropdown so that it is aligned with cursor
- this.updateDropdownOffset(this.currentDropdown);
- }
-
- destroyDroplab() {
- this.droplab.destroy();
- }
- }
-
- window.gl = window.gl || {};
- gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
new file mode 100644
index 00000000000..c6bb7fda8f2
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -0,0 +1,423 @@
+import FilteredSearchContainer from './container';
+
+(() => {
+ class FilteredSearchManager {
+ constructor(page) {
+ this.container = FilteredSearchContainer.container;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.clearSearchButton = this.container.querySelector('.clear-search');
+ this.tokensContainer = this.container.querySelector('.tokens-container');
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
+
+ if (this.filteredSearchInput) {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
+
+ this.bindEvents();
+ this.loadSearchParamsFromURL();
+ this.dropdownManager.setDropdown();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+ }
+
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ bindEvents() {
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
+ this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+ this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
+ this.checkForEnterWrapper = this.checkForEnter.bind(this);
+ this.clearSearchWrapper = this.clearSearch.bind(this);
+ this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
+ this.editTokenWrapper = this.editToken.bind(this);
+ this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
+
+ this.filteredSearchInputForm = this.filteredSearchInput.form;
+ this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.addEventListener('click', this.tokenChange);
+ this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
+ document.addEventListener('keydown', this.removeSelectedTokenWrapper);
+ }
+
+ unbindEvents() {
+ this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
+ this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
+ this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
+ this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
+ document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
+ }
+
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ }
+
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
+ }
+
+ checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
+
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
+
+ if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
+ e.preventDefault();
+
+ if (!activeElements.length) {
+ if (this.isHandledAsync) {
+ e.stopImmediatePropagation();
+
+ this.filteredSearchInput.blur();
+ this.dropdownManager.resetDropdowns();
+ } else {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
+ }
+
+ this.search();
+ }
+ }
+ }
+
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
+ }
+ }
+
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
+ }
+ }
+
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
+
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
+ }
+ }
+
+ unselectEditTokens(e) {
+ const inputContainer = this.container.querySelector('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementTokensContainer = e.target.classList.contains('tokens-container');
+
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
+ }
+ }
+
+ editToken(e) {
+ const token = e.target.closest('.js-visual-token');
+
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ this.tokenChange();
+ }
+ }
+
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
+
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
+ }
+ }
+
+ handleInputPlaceholder() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const placeholder = 'Search or filter results...';
+ const currentPlaceholder = this.filteredSearchInput.placeholder;
+
+ if (query.length === 0 && currentPlaceholder !== placeholder) {
+ this.filteredSearchInput.placeholder = placeholder;
+ } else if (query.length > 0 && currentPlaceholder !== '') {
+ this.filteredSearchInput.placeholder = '';
+ }
+ }
+
+ removeSelectedToken(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
+ }
+ }
+
+ clearSearch(e) {
+ e.preventDefault();
+
+ this.filteredSearchInput.value = '';
+
+ const removeElements = [];
+
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
+
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
+
+ this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
+
+ this.dropdownManager.resetDropdowns();
+
+ if (this.isHandledAsync) {
+ this.search();
+ }
+ }
+
+ handleInputVisualToken() {
+ const input = this.filteredSearchInput;
+ const { tokens, searchToken }
+ = gl.FilteredSearchTokenizer.processTokens(input.value);
+ const { isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (isLastVisualTokenValid) {
+ tokens.forEach((t) => {
+ input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ });
+
+ const fragments = searchToken.split(':');
+ if (fragments.length > 1) {
+ const inputValues = fragments[0].split(' ');
+ const tokenKey = inputValues.last();
+
+ if (inputValues.length > 1) {
+ inputValues.pop();
+ const searchTerms = inputValues.join(' ');
+
+ input.value = input.value.replace(searchTerms, '');
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
+ }
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+ input.value = input.value.replace(`${tokenKey}:`, '');
+ }
+ } else {
+ // Keep listening to token until we determine that the user is done typing the token value
+ const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
+
+ if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+
+ // Trim the last space as seen in the if statement above
+ input.value = input.value.replace(searchToken, '').trim();
+ }
+ }
+ }
+
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
+
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ let hasFilteredSearch = false;
+
+ params.forEach((p) => {
+ const split = p.split('=');
+ const keyParam = decodeURIComponent(split[0]);
+ const value = split[1];
+
+ // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
+ const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
+
+ if (condition) {
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
+ } else {
+ // Sanitize value since URL converts spaces into +
+ // Replace before decode so that we know what was originally + versus the encoded +
+ const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+ const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+ if (match) {
+ const indexOf = keyParam.indexOf('_');
+ const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ const symbol = match.symbol;
+ let quotationsToUse = '';
+
+ if (sanitizedValue.indexOf(' ') !== -1) {
+ // Prefer ", but use ' if required
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ }
+
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ } else if (!match && keyParam === 'assignee_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'author_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'search') {
+ hasFilteredSearch = true;
+ this.filteredSearchInput.value = sanitizedValue;
+ }
+ }
+ });
+
+ if (hasFilteredSearch) {
+ this.clearSearchButton.classList.remove('hidden');
+ this.handleInputPlaceholder();
+ }
+ }
+
+ search() {
+ const paths = [];
+ const { tokens, searchToken }
+ = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery());
+ const currentState = gl.utils.getParameterByName('state') || 'opened';
+ paths.push(`state=${currentState}`);
+
+ tokens.forEach((token) => {
+ const condition = this.filteredSearchTokenKeys
+ .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
+ const keyParam = param ? `${token.key}_${param}` : token.key;
+ let tokenPath = '';
+
+ if (condition) {
+ tokenPath = condition.url;
+ } else {
+ let tokenValue = token.value;
+
+ if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ tokenValue = tokenValue.slice(1, tokenValue.length - 1);
+ }
+
+ tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+ }
+
+ paths.push(tokenPath);
+ });
+
+ if (searchToken) {
+ const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ paths.push(`search=${sanitized}`);
+ }
+
+ const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
+
+ if (this.updateObject) {
+ this.updateObject(parameterizedUrl);
+ } else {
+ gl.utils.visitUrl(parameterizedUrl);
+ }
+ }
+
+ getUsernameParams() {
+ const usernamesById = {};
+ try {
+ const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ JSON.parse(attribute).forEach((user) => {
+ usernamesById[user.id] = user.username;
+ });
+ } catch (e) {
+ // do nothing
+ }
+ return usernamesById;
+ }
+
+ tokenChange() {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+
+ if (dropdown) {
+ const currentDropdownRef = dropdown.reference;
+
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchManager = FilteredSearchManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
deleted file mode 100644
index bbafead0305..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ /dev/null
@@ -1,231 +0,0 @@
-(() => {
- class FilteredSearchManager {
- constructor(page) {
- this.filteredSearchInput = document.querySelector('.filtered-search');
- this.clearSearchButton = document.querySelector('.clear-search');
- this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
-
- if (this.filteredSearchInput) {
- this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
-
- this.bindEvents();
- this.loadSearchParamsFromURL();
- this.dropdownManager.setDropdown();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
- }
-
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
-
- bindEvents() {
- this.handleFormSubmit = this.handleFormSubmit.bind(this);
- this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
- this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
- this.checkForEnterWrapper = this.checkForEnter.bind(this);
- this.clearSearchWrapper = this.clearSearch.bind(this);
- this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
- this.tokenChange = this.tokenChange.bind(this);
-
- this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.addEventListener('click', this.tokenChange);
- this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
- this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
- }
-
- unbindEvents() {
- this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
- this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
- this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
- this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
- this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
- this.filteredSearchInput.removeEventListener('click', this.tokenChange);
- this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
- this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
- }
-
- checkForBackspace(e) {
- // 8 = Backspace Key
- // 46 = Delete Key
- if (e.keyCode === 8 || e.keyCode === 46) {
- // Reposition dropdown so that it is aligned with cursor
- this.dropdownManager.updateCurrentDropdownOffset();
- }
- }
-
- checkForEnter(e) {
- if (e.keyCode === 38 || e.keyCode === 40) {
- const selectionStart = this.filteredSearchInput.selectionStart;
-
- e.preventDefault();
- this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
- }
-
- if (e.keyCode === 13) {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- const dropdownEl = dropdown.element;
- const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
-
- e.preventDefault();
-
- if (!activeElements.length) {
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
-
- this.search();
- }
- }
- }
-
- toggleClearSearchButton(e) {
- if (e.target.value) {
- this.clearSearchButton.classList.remove('hidden');
- } else {
- this.clearSearchButton.classList.add('hidden');
- }
- }
-
- clearSearch(e) {
- e.preventDefault();
-
- this.filteredSearchInput.value = '';
- this.clearSearchButton.classList.add('hidden');
-
- this.dropdownManager.resetDropdowns();
- }
-
- handleFormSubmit(e) {
- e.preventDefault();
- this.search();
- }
-
- loadSearchParamsFromURL() {
- const params = gl.utils.getUrlParamsArray();
- const usernameParams = this.getUsernameParams();
- const inputValues = [];
-
- params.forEach((p) => {
- const split = p.split('=');
- const keyParam = decodeURIComponent(split[0]);
- const value = split[1];
-
- // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
- const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
-
- if (condition) {
- inputValues.push(`${condition.tokenKey}:${condition.value}`);
- } else {
- // Sanitize value since URL converts spaces into +
- // Replace before decode so that we know what was originally + versus the encoded +
- const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
- const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
-
- if (match) {
- const indexOf = keyParam.indexOf('_');
- const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
- const symbol = match.symbol;
- let quotationsToUse = '';
-
- if (sanitizedValue.indexOf(' ') !== -1) {
- // Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
- }
-
- inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
- } else if (!match && keyParam === 'assignee_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- inputValues.push(`assignee:@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'author_id') {
- const id = parseInt(value, 10);
- if (usernameParams[id]) {
- inputValues.push(`author:@${usernameParams[id]}`);
- }
- } else if (!match && keyParam === 'search') {
- inputValues.push(sanitizedValue);
- }
- }
- });
-
- // Trim the last space value
- this.filteredSearchInput.value = inputValues.join(' ');
-
- if (inputValues.length > 0) {
- this.clearSearchButton.classList.remove('hidden');
- }
- }
-
- search() {
- const paths = [];
- const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
- const currentState = gl.utils.getParameterByName('state') || 'opened';
- paths.push(`state=${currentState}`);
-
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
- const keyParam = param ? `${token.key}_${param}` : token.key;
- let tokenPath = '';
-
- if (condition) {
- tokenPath = condition.url;
- } else {
- let tokenValue = token.value;
-
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
- tokenValue = tokenValue.slice(1, tokenValue.length - 1);
- }
-
- tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
- }
-
- paths.push(tokenPath);
- });
-
- if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
- paths.push(`search=${sanitized}`);
- }
-
- const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
-
- gl.utils.visitUrl(parameterizedUrl);
- }
-
- getUsernameParams() {
- const usernamesById = {};
- try {
- const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
- usernamesById[user.id] = user.username;
- });
- } catch (e) {
- // do nothing
- }
- return usernamesById;
- }
-
- tokenChange() {
- const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- const currentDropdownRef = dropdown.reference;
-
- this.setDropdownWrapper();
- currentDropdownRef.dispatchInputEvent();
- }
- }
-
- window.gl = window.gl || {};
- gl.FilteredSearchManager = FilteredSearchManager;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
new file mode 100644
index 00000000000..6d5df86f2a5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -0,0 +1,100 @@
+(() => {
+ const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+ }, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ }];
+
+ const alternativeTokenKeys = [{
+ key: 'label',
+ type: 'string',
+ param: 'name',
+ symbol: '~',
+ }];
+
+ const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
+
+ const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+ }, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+ }, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+ }, {
+ url: 'milestone_title=%23started',
+ tokenKey: 'milestone',
+ value: 'started',
+ }, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+ }];
+
+ class FilteredSearchTokenKeys {
+ static get() {
+ return tokenKeys;
+ }
+
+ static getAlternatives() {
+ return alternativeTokenKeys;
+ }
+
+ static getConditions() {
+ return conditions;
+ }
+
+ static searchByKey(key) {
+ return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ }
+
+ static searchBySymbol(symbol) {
+ return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ }
+
+ static searchByKeyParam(keyParam) {
+ return tokenKeysWithAlternative.find((tokenKey) => {
+ let tokenKeyParam = tokenKey.key;
+
+ if (tokenKey.param) {
+ tokenKeyParam += `_${tokenKey.param}`;
+ }
+
+ return keyParam === tokenKeyParam;
+ }) || null;
+ }
+
+ static searchByConditionUrl(url) {
+ return conditions.find(condition => condition.url === url) || null;
+ }
+
+ static searchByConditionKeyValue(key, value) {
+ return conditions
+ .find(condition => condition.tokenKey === key && condition.value === value) || null;
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
deleted file mode 100644
index e6b53cd4b55..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
+++ /dev/null
@@ -1,96 +0,0 @@
-(() => {
- const tokenKeys = [{
- key: 'author',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'assignee',
- type: 'string',
- param: 'username',
- symbol: '@',
- }, {
- key: 'milestone',
- type: 'string',
- param: 'title',
- symbol: '%',
- }, {
- key: 'label',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- }];
-
- const alternativeTokenKeys = [{
- key: 'label',
- type: 'string',
- param: 'name',
- symbol: '~',
- }];
-
- const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
-
- const conditions = [{
- url: 'assignee_id=0',
- tokenKey: 'assignee',
- value: 'none',
- }, {
- url: 'milestone_title=No+Milestone',
- tokenKey: 'milestone',
- value: 'none',
- }, {
- url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
- value: 'upcoming',
- }, {
- url: 'label_name[]=No+Label',
- tokenKey: 'label',
- value: 'none',
- }];
-
- class FilteredSearchTokenKeys {
- static get() {
- return tokenKeys;
- }
-
- static getAlternatives() {
- return alternativeTokenKeys;
- }
-
- static getConditions() {
- return conditions;
- }
-
- static searchByKey(key) {
- return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
- }
-
- static searchBySymbol(symbol) {
- return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
- }
-
- static searchByKeyParam(keyParam) {
- return tokenKeysWithAlternative.find((tokenKey) => {
- let tokenKeyParam = tokenKey.key;
-
- if (tokenKey.param) {
- tokenKeyParam += `_${tokenKey.param}`;
- }
-
- return keyParam === tokenKeyParam;
- }) || null;
- }
-
- static searchByConditionUrl(url) {
- return conditions.find(condition => condition.url === url) || null;
- }
-
- static searchByConditionKeyValue(key, value) {
- return conditions
- .find(condition => condition.tokenKey === key && condition.value === value) || null;
- }
- }
-
- window.gl = window.gl || {};
- gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
-})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index 9bf1b1ced88..9bf1b1ced88 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
new file mode 100644
index 00000000000..a5657fc8720
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -0,0 +1,202 @@
+import FilteredSearchContainer from './container';
+
+class FilteredSearchVisualTokens {
+ static getLastVisualTokenBeforeInput() {
+ const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
+ const lastVisualToken = inputLi && inputLi.previousElementSibling;
+
+ return {
+ lastVisualToken,
+ isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
+ };
+ }
+
+ static unselectTokens() {
+ const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
+ [].forEach.call(otherTokens, t => t.classList.remove('selected'));
+ }
+
+ static selectToken(tokenButton) {
+ const selected = tokenButton.classList.contains('selected');
+ FilteredSearchVisualTokens.unselectTokens();
+
+ if (!selected) {
+ tokenButton.classList.add('selected');
+ }
+ }
+
+ static removeSelectedToken() {
+ const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
+
+ if (selected) {
+ const li = selected.closest('.js-visual-token');
+ li.parentElement.removeChild(li);
+ }
+ }
+
+ static createVisualTokenElementHTML() {
+ return `
+ <div class="selectable" role="button">
+ <div class="name"></div>
+ <div class="value"></div>
+ </div>
+ `;
+ }
+
+ static addVisualTokenElement(name, value, isSearchTerm) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token');
+ li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+
+ if (value) {
+ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ li.querySelector('.value').innerText = value;
+ } else {
+ li.innerHTML = '<div class="name"></div>';
+ }
+ li.querySelector('.name').innerText = name;
+
+ const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ tokensContainer.insertBefore(li, input.parentElement);
+ }
+
+ static addValueToPreviousVisualTokenElement(value) {
+ const { lastVisualToken, isLastVisualTokenValid } =
+ FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
+ const name = FilteredSearchVisualTokens.getLastTokenPartial();
+ lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ lastVisualToken.querySelector('.name').innerText = name;
+ lastVisualToken.querySelector('.value').innerText = value;
+ }
+ }
+
+ static addFilterVisualToken(tokenName, tokenValue) {
+ const { lastVisualToken, isLastVisualTokenValid }
+ = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+
+ if (isLastVisualTokenValid) {
+ addVisualTokenElement(tokenName, tokenValue, false);
+ } else {
+ const previousTokenName = lastVisualToken.querySelector('.name').innerText;
+ const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+ tokensContainer.removeChild(lastVisualToken);
+
+ const value = tokenValue || tokenName;
+ addVisualTokenElement(previousTokenName, value, false);
+ }
+ }
+
+ static addSearchVisualToken(searchTerm) {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
+ } else {
+ FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ }
+ }
+
+ static getLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!lastVisualToken) return '';
+
+ const value = lastVisualToken.querySelector('.value');
+ const name = lastVisualToken.querySelector('.name');
+
+ const valueText = value ? value.innerText : '';
+ const nameText = name ? name.innerText : '';
+
+ return valueText || nameText;
+ }
+
+ static removeLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken) {
+ const value = lastVisualToken.querySelector('.value');
+
+ if (value) {
+ const button = lastVisualToken.querySelector('.selectable');
+ button.removeChild(value);
+ lastVisualToken.innerHTML = button.innerHTML;
+ } else {
+ lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
+ }
+ }
+ }
+
+ static tokenizeInput() {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (input.value) {
+ if (isLastVisualTokenValid) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
+ } else {
+ FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
+ }
+
+ input.value = '';
+ }
+ }
+
+ static editToken(token) {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ // Replace token with input field
+ const tokenContainer = token.parentElement;
+ const inputLi = input.parentElement;
+ tokenContainer.replaceChild(inputLi, token);
+
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+
+ if (token.classList.contains('filtered-search-token') && value) {
+ FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
+ input.value = value.innerText;
+ } else {
+ // token is a search term
+ input.value = name.innerText;
+ }
+
+ // Opens dropdown
+ const inputEvent = new Event('input');
+ input.dispatchEvent(inputEvent);
+
+ // Adds cursor to input
+ input.focus();
+ }
+
+ static moveInputToTheRight() {
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const inputLi = input.parentElement;
+ const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid) {
+ const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
+ }
+
+ tokenContainer.removeChild(inputLi);
+ tokenContainer.appendChild(inputLi);
+ }
+ }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 730104b89f9..eec30624ff2 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,42 +1,41 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
-(function() {
- this.Flash = (function() {
- var hideFlash;
- hideFlash = function() {
- return $(this).fadeOut();
- };
+window.Flash = (function() {
+ var hideFlash;
- function Flash(message, type, parent) {
- var flash, textDiv;
- if (type == null) {
- type = 'alert';
- }
- if (parent == null) {
- parent = null;
- }
- if (parent) {
- this.flashContainer = parent.find('.flash-container');
- } else {
- this.flashContainer = $('.flash-container-page');
- }
- this.flashContainer.html('');
- flash = $('<div/>', {
- "class": "flash-" + type
- });
- flash.on('click', hideFlash);
- textDiv = $('<div/>', {
- "class": 'flash-text',
- text: message
- });
- textDiv.appendTo(flash);
- if (this.flashContainer.parent().hasClass('content-wrapper')) {
- textDiv.addClass('container-fluid container-limited');
- }
- flash.appendTo(this.flashContainer);
- this.flashContainer.show();
+ hideFlash = function() {
+ return $(this).fadeOut();
+ };
+
+ function Flash(message, type, parent) {
+ var flash, textDiv;
+ if (type == null) {
+ type = 'alert';
+ }
+ if (parent == null) {
+ parent = null;
+ }
+ if (parent) {
+ this.flashContainer = parent.find('.flash-container');
+ } else {
+ this.flashContainer = $('.flash-container-page');
+ }
+ this.flashContainer.html('');
+ flash = $('<div/>', {
+ "class": "flash-" + type
+ });
+ flash.on('click', hideFlash);
+ textDiv = $('<div/>', {
+ "class": 'flash-text',
+ text: message
+ });
+ textDiv.appendTo(flash);
+ if (this.flashContainer.parent().hasClass('content-wrapper')) {
+ textDiv.addClass('container-fluid container-limited');
}
+ flash.appendTo(this.flashContainer);
+ this.flashContainer.show();
+ }
- return Flash;
- })();
-}).call(window);
+ return Flash;
+})();
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
new file mode 100644
index 00000000000..9ac4c49d697
--- /dev/null
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -0,0 +1,390 @@
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
+
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+import { glEmojiTag } from '~/behaviors/gl_emoji';
+
+// Creates the variables for setting up GFM auto-completion
+window.gl = window.gl || {};
+
+function sanitize(str) {
+ return str.replace(/<(?:.|\n)*?>/gm, '');
+}
+
+window.gl.GfmAutoComplete = {
+ dataSources: {},
+ defaultLoadingData: ['loading'],
+ cachedData: {},
+ isLoadingData: {},
+ atTypeMap: {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+ },
+ // Emoji
+ Emoji: {
+ templateFunction: function(name) {
+ return `<li>
+ ${name} ${glEmojiTag(name)}
+ </li>
+ `;
+ }
+ },
+ // Team Members
+ Members: {
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
+ },
+ Labels: {
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+ },
+ // Issues and MergeRequests
+ Issues: {
+ template: '<li><small>${id}</small> ${title}</li>'
+ },
+ // Milestones
+ Milestones: {
+ template: '<li>${title}</li>'
+ },
+ Loading: {
+ template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ },
+ DefaultOptions: {
+ sorter: function(query, items, searchKey) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+ if (gl.GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
+ return items;
+ }
+ return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
+ },
+ filter: function(query, data, searchKey) {
+ if (gl.GfmAutoComplete.isLoading(data)) {
+ gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
+ return data;
+ } else {
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
+ }
+ },
+ beforeInsert: function(value) {
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
+ }
+ return value;
+ },
+ matcher: function (flag, subtext) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
+ atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ subtext = subtext.split(/\s+/g).pop();
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
+
+ match = regexp.exec(subtext);
+
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+ },
+ setup: function(input) {
+ // Add GFM auto-completion to all input fields, that accept GFM input.
+ this.input = input || $('.js-gfm-input');
+ this.setupLifecycle();
+ },
+ setupLifecycle() {
+ this.input.each((i, input) => {
+ const $input = $(input);
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ // This triggers at.js again
+ // Needed for slash commands with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ });
+ },
+ setupAtWho: function($input) {
+ // Emoji
+ $input.atwho({
+ at: ':',
+ displayTpl: function(value) {
+ return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
+ }.bind(this),
+ insertTpl: ':${name}:',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter
+ }
+ });
+ // Team Members
+ $input.atwho({
+ at: '@',
+ displayTpl: function(value) {
+ return value.username != null ? this.Members.template : this.Loading.template;
+ }.bind(this),
+ insertTpl: '${atwho-at}${username}',
+ searchKey: 'search',
+ alwaysHighlightFirst: true,
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(members) {
+ return $.map(members, function(m) {
+ let title = '';
+ if (m.username == null) {
+ return m;
+ }
+ title = m.name;
+ if (m.count) {
+ title += " (" + m.count + ")";
+ }
+
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
+ return {
+ username: m.username,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ title: sanitize(title),
+ search: sanitize(m.username + " " + m.name)
+ };
+ });
+ }
+ }
+ });
+ $input.atwho({
+ at: '#',
+ alias: 'issues',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(issues) {
+ return $.map(issues, function(i) {
+ if (i.title == null) {
+ return i;
+ }
+ return {
+ id: i.iid,
+ title: sanitize(i.title),
+ search: i.iid + " " + i.title
+ };
+ });
+ }
+ }
+ });
+ $input.atwho({
+ at: '%',
+ alias: 'milestones',
+ searchKey: 'search',
+ insertTpl: '${atwho-at}${title}',
+ displayTpl: function(value) {
+ return value.title != null ? this.Milestones.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
+ beforeSave: function(milestones) {
+ return $.map(milestones, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: "" + m.title
+ };
+ });
+ }
+ }
+ });
+ $input.atwho({
+ at: '!',
+ alias: 'mergerequests',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ return value.title != null ? this.Issues.template : this.Loading.template;
+ }.bind(this),
+ data: this.defaultLoadingData,
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ matcher: this.DefaultOptions.matcher,
+ beforeSave: function(merges) {
+ return $.map(merges, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: m.iid + " " + m.title
+ };
+ });
+ }
+ }
+ });
+ $input.atwho({
+ at: '~',
+ alias: 'labels',
+ searchKey: 'search',
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+ }.bind(this),
+ insertTpl: '${atwho-at}${title}',
+ callbacks: {
+ matcher: this.DefaultOptions.matcher,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter,
+ sorter: this.DefaultOptions.sorter,
+ beforeSave: function(merges) {
+ if (gl.GfmAutoComplete.isLoading(merges)) return merges;
+ var sanitizeLabelTitle;
+ sanitizeLabelTitle = function(title) {
+ if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
+ return "\"" + (sanitize(title)) + "\"";
+ } else {
+ return sanitize(title);
+ }
+ };
+ return $.map(merges, function(m) {
+ return {
+ title: sanitize(m.title),
+ color: m.color,
+ search: "" + m.title
+ };
+ });
+ }
+ }
+ });
+ // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
+ at: '/',
+ alias: 'commands',
+ searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
+ displayTpl: function(value) {
+ if (this.isLoading(value)) return this.Loading.template;
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ }.bind(this),
+ insertTpl: function(value) {
+ var tpl = "/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
+ }
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(commands) {
+ if (gl.GfmAutoComplete.isLoading(commands)) return commands;
+ return $.map(commands, function(c) {
+ var search = c.name;
+ if (c.aliases.length > 0) {
+ search = search + " " + c.aliases.join(" ");
+ }
+ return {
+ name: c.name,
+ aliases: c.aliases,
+ params: c.params,
+ description: c.description,
+ search: search
+ };
+ });
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
+ var match = regexp.exec(subtext);
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+ }
+ });
+ return;
+ },
+ fetchData: function($input, at) {
+ if (this.isLoadingData[at]) return;
+ this.isLoadingData[at] = true;
+ if (this.cachedData[at]) {
+ this.loadData($input, at, this.cachedData[at]);
+ } else if (this.atTypeMap[at] === 'emojis') {
+ this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
+ } else {
+ $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ this.loadData($input, at, data);
+ }).fail(() => { this.isLoadingData[at] = false; });
+ }
+ },
+ loadData: function($input, at, data) {
+ this.isLoadingData[at] = false;
+ this.cachedData[at] = data;
+ $input.atwho('load', at, data);
+ // This trigger at.js again
+ // otherwise we would be stuck with loading until the user types
+ return $input.trigger('keyup');
+ },
+ isLoading(data) {
+ var dataToInspect = data;
+ if (data && data.length > 0) {
+ dataToInspect = data[0];
+ }
+
+ var loadingState = this.defaultLoadingData[0];
+ return dataToInspect &&
+ (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ }
+};
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
deleted file mode 100644
index 60d6658dc16..00000000000
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ /dev/null
@@ -1,383 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
-
-// Creates the variables for setting up GFM auto-completion
-(function() {
- if (window.gl == null) {
- window.gl = {};
- }
-
- function sanitize(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
- }
-
- window.gl.GfmAutoComplete = {
- dataSources: {},
- defaultLoadingData: ['loading'],
- cachedData: {},
- isLoadingData: {},
- atTypeMap: {
- ':': 'emojis',
- '@': 'members',
- '#': 'issues',
- '!': 'mergeRequests',
- '~': 'labels',
- '%': 'milestones',
- '/': 'commands'
- },
- // Emoji
- Emoji: {
- template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
- },
- // Team Members
- Members: {
- template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
- },
- Labels: {
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
- },
- // Issues and MergeRequests
- Issues: {
- template: '<li><small>${id}</small> ${title}</li>'
- },
- // Milestones
- Milestones: {
- template: '<li>${title}</li>'
- },
- Loading: {
- template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
- },
- DefaultOptions: {
- sorter: function(query, items, searchKey) {
- this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
- if (gl.GfmAutoComplete.isLoading(items)) {
- this.setting.highlightFirst = false;
- return items;
- }
- return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
- },
- filter: function(query, data, searchKey) {
- if (gl.GfmAutoComplete.isLoading(data)) {
- gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
- return data;
- } else {
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
- }
- },
- beforeInsert: function(value) {
- if (value && !this.setting.skipSpecialCharacterTest) {
- var withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
- }
- return value;
- },
- matcher: function (flag, subtext) {
- // The below is taken from At.js source
- // Tweaked to commands to start without a space only if char before is a non-word character
- // https://github.com/ichord/At.js
- var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
- atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
- atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
- subtext = subtext.split(/\s+/g).pop();
- flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-
- _a = decodeURI("%C3%80");
- _y = decodeURI("%C3%BF");
-
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
-
- match = regexp.exec(subtext);
-
- if (match) {
- return match[1];
- } else {
- return null;
- }
- }
- },
- setup: function(input) {
- // Add GFM auto-completion to all input fields, that accept GFM input.
- this.input = input || $('.js-gfm-input');
- this.setupLifecycle();
- },
- setupLifecycle() {
- this.input.each((i, input) => {
- const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- // This triggers at.js again
- // Needed for slash commands with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- });
- },
- setupAtWho: function($input) {
- // Emoji
- $input.atwho({
- at: ':',
- displayTpl: function(value) {
- return value.path != null ? this.Emoji.template : this.Loading.template;
- }.bind(this),
- insertTpl: ':${name}:',
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter
- }
- });
- // Team Members
- $input.atwho({
- at: '@',
- displayTpl: function(value) {
- return value.username != null ? this.Members.template : this.Loading.template;
- }.bind(this),
- insertTpl: '${atwho-at}${username}',
- searchKey: 'search',
- alwaysHighlightFirst: true,
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(members) {
- return $.map(members, function(m) {
- let title = '';
- if (m.username == null) {
- return m;
- }
- title = m.name;
- if (m.count) {
- title += " (" + m.count + ")";
- }
-
- const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
- const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
-
- return {
- username: m.username,
- avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
- title: sanitize(title),
- search: sanitize(m.username + " " + m.name)
- };
- });
- }
- }
- });
- $input.atwho({
- at: '#',
- alias: 'issues',
- searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- insertTpl: '${atwho-at}${id}',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(issues) {
- return $.map(issues, function(i) {
- if (i.title == null) {
- return i;
- }
- return {
- id: i.iid,
- title: sanitize(i.title),
- search: i.iid + " " + i.title
- };
- });
- }
- }
- });
- $input.atwho({
- at: '%',
- alias: 'milestones',
- searchKey: 'search',
- insertTpl: '${atwho-at}${title}',
- displayTpl: function(value) {
- return value.title != null ? this.Milestones.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- callbacks: {
- matcher: this.DefaultOptions.matcher,
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- beforeSave: function(milestones) {
- return $.map(milestones, function(m) {
- if (m.title == null) {
- return m;
- }
- return {
- id: m.iid,
- title: sanitize(m.title),
- search: "" + m.title
- };
- });
- }
- }
- });
- $input.atwho({
- at: '!',
- alias: 'mergerequests',
- searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
- insertTpl: '${atwho-at}${id}',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(merges) {
- return $.map(merges, function(m) {
- if (m.title == null) {
- return m;
- }
- return {
- id: m.iid,
- title: sanitize(m.title),
- search: m.iid + " " + m.title
- };
- });
- }
- }
- });
- $input.atwho({
- at: '~',
- alias: 'labels',
- searchKey: 'search',
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- return this.isLoading(value) ? this.Loading.template : this.Labels.template;
- }.bind(this),
- insertTpl: '${atwho-at}${title}',
- callbacks: {
- matcher: this.DefaultOptions.matcher,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- sorter: this.DefaultOptions.sorter,
- beforeSave: function(merges) {
- if (gl.GfmAutoComplete.isLoading(merges)) return merges;
- var sanitizeLabelTitle;
- sanitizeLabelTitle = function(title) {
- if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
- return "\"" + (sanitize(title)) + "\"";
- } else {
- return sanitize(title);
- }
- };
- return $.map(merges, function(m) {
- return {
- title: sanitize(m.title),
- color: m.color,
- search: "" + m.title
- };
- });
- }
- }
- });
- // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- $input.filter('[data-supports-slash-commands="true"]').atwho({
- at: '/',
- alias: 'commands',
- searchKey: 'search',
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- if (this.isLoading(value)) return this.Loading.template;
- var tpl = '<li>/${name}';
- if (value.aliases.length > 0) {
- tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
- }
- if (value.params.length > 0) {
- tpl += ' <small><%- params.join(" ") %></small>';
- }
- if (value.description !== '') {
- tpl += '<small class="description"><i><%- description %></i></small>';
- }
- tpl += '</li>';
- return _.template(tpl)(value);
- }.bind(this),
- insertTpl: function(value) {
- var tpl = "/${name} ";
- var reference_prefix = null;
- if (value.params.length > 0) {
- reference_prefix = value.params[0][0];
- if (/^[@%~]/.test(reference_prefix)) {
- tpl += '<%- reference_prefix %>';
- }
- }
- return _.template(tpl)({ reference_prefix: reference_prefix });
- },
- suffix: '',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- beforeSave: function(commands) {
- if (gl.GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, function(c) {
- var search = c.name;
- if (c.aliases.length > 0) {
- search = search + " " + c.aliases.join(" ");
- }
- return {
- name: c.name,
- aliases: c.aliases,
- params: c.params,
- description: c.description,
- search: search
- };
- });
- },
- matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
- var match = regexp.exec(subtext);
- if (match) {
- return match[1];
- } else {
- return null;
- }
- }
- }
- });
- return;
- },
- fetchData: function($input, at) {
- if (this.isLoadingData[at]) return;
- this.isLoadingData[at] = true;
- if (this.cachedData[at]) {
- this.loadData($input, at, this.cachedData[at]);
- } else {
- $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
- this.loadData($input, at, data);
- }).fail(() => { this.isLoadingData[at] = false; });
- }
- },
- loadData: function($input, at, data) {
- this.isLoadingData[at] = false;
- this.cachedData[at] = data;
- $input.atwho('load', at, data);
- // This trigger at.js again
- // otherwise we would be stuck with loading until the user types
- return $input.trigger('keyup');
- },
- isLoading(data) {
- var dataToInspect = data;
- if (data && data.length > 0) {
- dataToInspect = data[0];
- }
-
- var loadingState = this.defaultLoadingData[0];
- return dataToInspect &&
- (dataToInspect === loadingState || dataToInspect.name === loadingState);
- }
- };
-}).call(window);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a01662e2f9e..a03f1202a6d 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,850 +1,848 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
-(function() {
- var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
- bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
-
- GitLabDropdownFilter = (function() {
- var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
-
- BLUR_KEYCODES = [27, 40];
-
- ARROW_KEY_CODES = [38, 40];
-
- HAS_VALUE_CLASS = "has-value";
-
- function GitLabDropdownFilter(input, options) {
- var $clearButton, $inputContainer, ref, timeout;
- this.input = input;
- this.options = options;
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- $inputContainer = this.input.parent();
- $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', (function(_this) {
- // Clear click
- return function(e) {
+var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
+ bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
+
+GitLabDropdownFilter = (function() {
+ var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
+
+ BLUR_KEYCODES = [27, 40];
+
+ ARROW_KEY_CODES = [38, 40];
+
+ HAS_VALUE_CLASS = "has-value";
+
+ function GitLabDropdownFilter(input, options) {
+ var $clearButton, $inputContainer, ref, timeout;
+ this.input = input;
+ this.options = options;
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ $inputContainer = this.input.parent();
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input.val('').trigger('input').focus();
+ };
+ })(this));
+ // Key events
+ timeout = "";
+ this.input
+ .on('keydown', function (e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
- e.stopPropagation();
- return _this.input.val('').trigger('input').focus();
- };
- })(this));
- // Key events
- timeout = "";
- this.input
- .on('keydown', function (e) {
- var keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', function() {
- if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- return timeout = setTimeout(function() {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), function(data) {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- }.bind(this));
- }.bind(this), 250);
- } else {
- return this.filter(this.input.val());
- }
- }.bind(this));
- }
+ }
+ })
+ .on('input', function() {
+ if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ return timeout = setTimeout(function() {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), function(data) {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ }.bind(this));
+ }.bind(this), 250);
+ } else {
+ return this.filter(this.input.val());
+ }
+ }.bind(this));
+ }
- GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) >= 0;
- };
+ GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+ };
- GitLabDropdownFilter.prototype.filter = function(search_text) {
- var data, elements, group, key, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(search_text);
- }
- data = this.options.data();
- if ((data != null) && !this.options.filterByText) {
- results = data;
- if (search_text !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (_.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, search_text, {
- key: this.options.keys
- });
- } else {
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- if (gl.utils.isObject(data)) {
- results = {};
- for (key in data) {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, search_text, {
- key: this.options.keys
+ GitLabDropdownFilter.prototype.filter = function(search_text) {
+ var data, elements, group, key, results, tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(search_text);
+ }
+ data = this.options.data();
+ if ((data != null) && !this.options.filterByText) {
+ results = data;
+ if (search_text !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (_.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, search_text, {
+ key: this.options.keys
+ });
+ } else {
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ if (gl.utils.isObject(data)) {
+ results = {};
+ for (key in data) {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, search_text, {
+ key: this.options.keys
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(function(item) {
+ return item;
});
- if (tmp.length) {
- results[key] = tmp.map(function(item) {
- return item;
- });
- }
}
}
}
}
- return this.options.callback(results);
- } else {
- elements = this.options.elements();
- if (search_text) {
- return elements.each(function() {
- var $el, matches;
- $el = $(this);
- matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- } else {
- return $el.hide().addClass('option-hidden');
- }
+ }
+ return this.options.callback(results);
+ } else {
+ elements = this.options.elements();
+ if (search_text) {
+ return elements.each(function() {
+ var $el, matches;
+ $el = $(this);
+ matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ } else {
+ return $el.hide().addClass('option-hidden');
}
- });
- } else {
- return elements.show().removeClass('option-hidden');
- }
+ }
+ });
+ } else {
+ return elements.show().removeClass('option-hidden');
}
- };
-
- return GitLabDropdownFilter;
- })();
+ }
+ };
- GitLabDropdownRemote = (function() {
- function GitLabDropdownRemote(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
+ return GitLabDropdownFilter;
+})();
+
+GitLabDropdownRemote = (function() {
+ function GitLabDropdownRemote(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ GitLabDropdownRemote.prototype.execute = function() {
+ if (typeof this.dataEndpoint === "string") {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === "function") {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint("", (function(_this) {
+ // Fetch the data by calling the data funcfion
+ return function(data) {
+ if (_this.options.success) {
+ _this.options.success(data);
+ }
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this));
}
+ };
- GitLabDropdownRemote.prototype.execute = function() {
- if (typeof this.dataEndpoint === "string") {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === "function") {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint("", (function(_this) {
- // Fetch the data by calling the data funcfion
- return function(data) {
- if (_this.options.success) {
- _this.options.success(data);
- }
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this));
- }
- };
+ GitLabDropdownRemote.prototype.fetchData = function() {
+ return $.ajax({
+ url: this.dataEndpoint,
+ dataType: this.options.dataType,
+ beforeSend: (function(_this) {
+ return function() {
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this),
+ success: (function(_this) {
+ return function(data) {
+ if (_this.options.success) {
+ return _this.options.success(data);
+ }
+ };
+ })(this)
+ });
+ // Fetch the data through ajax if the data is a string
+ };
- GitLabDropdownRemote.prototype.fetchData = function() {
- return $.ajax({
- url: this.dataEndpoint,
- dataType: this.options.dataType,
- beforeSend: (function(_this) {
- return function() {
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this),
- success: (function(_this) {
- return function(data) {
- if (_this.options.success) {
- return _this.options.success(data);
- }
- };
- })(this)
- });
- // Fetch the data through ajax if the data is a string
- };
+ return GitLabDropdownRemote;
+})();
- return GitLabDropdownRemote;
- })();
+GitLabDropdown = (function() {
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
- GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
+ LOADING_CLASS = "is-loading";
- LOADING_CLASS = "is-loading";
+ PAGE_TWO_CLASS = "is-page-two";
- PAGE_TWO_CLASS = "is-page-two";
+ ACTIVE_CLASS = "is-active";
- ACTIVE_CLASS = "is-active";
+ INDETERMINATE_CLASS = "is-indeterminate";
- INDETERMINATE_CLASS = "is-indeterminate";
+ currentIndex = -1;
- currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
- NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
-
- SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
-
- CURSOR_SELECT_SCROLL_PADDING = 5;
-
- FILTER_INPUT = '.dropdown-input .dropdown-input-field';
-
- function GitLabDropdown(el1, options) {
- var searchFields, selector, self;
- this.el = el1;
- this.options = options;
- this.updateLabel = bind(this.updateLabel, this);
- this.hidden = bind(this.hidden, this);
- this.opened = bind(this.opened, this);
- this.shouldPropagate = bind(this.shouldPropagate, this);
- self = this;
- selector = $(this.el).data("target");
- this.dropdown = selector != null ? $(selector) : $(this.el).parent();
- // Set Defaults
- this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.highlight = !!this.options.highlight;
- this.filterInputBlur = this.options.filterInputBlur != null
- ? this.options.filterInputBlur
- : true;
- // If no input is passed create a default one
- self = this;
- // If selector was passed
- if (_.isString(this.filterInput)) {
- this.filterInput = this.getElement(this.filterInput);
- }
- searchFields = this.options.search ? this.options.search.fields : [];
- if (this.options.data) {
- // If we provided data
- // data could be an array of objects or a group of arrays
- if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
- this.fullData = this.options.data;
- currentIndex = -1;
- this.parseData(this.options.data);
- this.focusTextInput();
- } else {
- this.remote = new GitLabDropdownRemote(this.options.data, {
- dataType: this.options.dataType,
- beforeSend: this.toggleLoading.bind(this),
- success: (function(_this) {
- return function(data) {
- _this.fullData = data;
- _this.parseData(_this.fullData);
- _this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
- return _this.filter.input.trigger('input');
- }
- };
- // Remote data
- })(this)
- });
- }
- }
- // Init filterable
- if (this.options.filterable) {
- this.filter = new GitLabDropdownFilter(this.filterInput, {
- elIsInput: $(this.el).is('input'),
- filterInputBlur: this.filterInputBlur,
- filterByText: this.options.filterByText,
- onFilter: this.options.onFilter,
- remote: this.options.filterRemote,
- query: this.options.data,
- keys: searchFields,
- elements: (function(_this) {
- return function() {
- selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
- }
- return $(selector);
- };
- })(this),
- data: (function(_this) {
- return function() {
- return _this.fullData;
- };
- })(this),
- callback: (function(_this) {
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5;
+
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+
+ function GitLabDropdown(el1, options) {
+ var searchFields, selector, self;
+ this.el = el1;
+ this.options = options;
+ this.updateLabel = bind(this.updateLabel, this);
+ this.hidden = bind(this.hidden, this);
+ this.opened = bind(this.opened, this);
+ this.shouldPropagate = bind(this.shouldPropagate, this);
+ self = this;
+ selector = $(this.el).data("target");
+ this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.highlight = !!this.options.highlight;
+ this.filterInputBlur = this.options.filterInputBlur != null
+ ? this.options.filterInputBlur
+ : true;
+ // If no input is passed create a default one
+ self = this;
+ // If selector was passed
+ if (_.isString(this.filterInput)) {
+ this.filterInput = this.getElement(this.filterInput);
+ }
+ searchFields = this.options.search ? this.options.search.fields : [];
+ if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
+ if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+ this.fullData = this.options.data;
+ currentIndex = -1;
+ this.parseData(this.options.data);
+ this.focusTextInput();
+ } else {
+ this.remote = new GitLabDropdownRemote(this.options.data, {
+ dataType: this.options.dataType,
+ beforeSend: this.toggleLoading.bind(this),
+ success: (function(_this) {
return function(data) {
- _this.parseData(data);
- if (_this.filterInput.val() !== '') {
- selector = SELECTABLE_CLASSES;
- if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
- }
- if ($(_this.el).is('input')) {
- currentIndex = -1;
- } else {
- $(selector, _this.dropdown).first().find('a').addClass('is-focused');
- currentIndex = 0;
- }
+ _this.fullData = data;
+ _this.parseData(_this.fullData);
+ _this.focusTextInput();
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
+ return _this.filter.input.trigger('input');
}
};
+ // Remote data
})(this)
});
}
- // Event listeners
- this.dropdown.on("shown.bs.dropdown", this.opened);
- this.dropdown.on("hidden.bs.dropdown", this.hidden);
- $(this.el).on("update.label", this.updateLabel);
- this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
- this.dropdown.on('keyup', (function(_this) {
- return function(e) {
- // Escape key
- if (e.which === 27) {
- return $('.dropdown-menu-close', _this.dropdown).trigger('click');
- }
- };
- })(this));
- this.dropdown.on('blur', 'a', (function(_this) {
- return function(e) {
- var $dropdownMenu, $relatedTarget;
- if (e.relatedTarget != null) {
- $relatedTarget = $(e.relatedTarget);
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
- if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('open');
+ }
+ // Init filterable
+ if (this.options.filterable) {
+ this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
+ filterInputBlur: this.filterInputBlur,
+ filterByText: this.options.filterByText,
+ onFilter: this.options.onFilter,
+ remote: this.options.filterRemote,
+ query: this.options.data,
+ keys: searchFields,
+ elements: (function(_this) {
+ return function() {
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $(selector);
+ };
+ })(this),
+ data: (function(_this) {
+ return function() {
+ return _this.fullData;
+ };
+ })(this),
+ callback: (function(_this) {
+ return function(data) {
+ _this.parseData(data);
+ if (_this.filterInput.val() !== '') {
+ selector = SELECTABLE_CLASSES;
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ if ($(_this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+ currentIndex = 0;
+ }
}
+ };
+ })(this)
+ });
+ }
+ // Event listeners
+ this.dropdown.on("shown.bs.dropdown", this.opened);
+ this.dropdown.on("hidden.bs.dropdown", this.hidden);
+ $(this.el).on("update.label", this.updateLabel);
+ this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
+ this.dropdown.on('keyup', (function(_this) {
+ return function(e) {
+ // Escape key
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', _this.dropdown).trigger('click');
+ }
+ };
+ })(this));
+ this.dropdown.on('blur', 'a', (function(_this) {
+ return function(e) {
+ var $dropdownMenu, $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return _this.dropdown.removeClass('open');
}
+ }
+ };
+ })(this));
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.togglePage();
};
})(this));
+ }
+ if (this.options.selectable) {
+ selector = ".dropdown-content a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
- this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.togglePage();
- };
- })(this));
- }
- if (this.options.selectable) {
- selector = ".dropdown-content a";
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content a";
+ selector = ".dropdown-page-one .dropdown-content a";
+ }
+ this.dropdown.on("click", selector, function(e) {
+ var $el, selected, selectedObj, isMarking;
+ $el = $(this);
+ selected = self.rowClicked($el);
+ selectedObj = selected ? selected[0] : null;
+ isMarking = selected ? selected[1] : null;
+ if (self.options.clicked) {
+ self.options.clicked(selectedObj, $el, e, isMarking);
}
- this.dropdown.on("click", selector, function(e) {
- var $el, selected, selectedObj, isMarking;
- $el = $(this);
- selected = self.rowClicked($el);
- selectedObj = selected ? selected[0] : null;
- isMarking = selected ? selected[1] : null;
- if (self.options.clicked) {
- self.options.clicked(selectedObj, $el, e, isMarking);
- }
- // Update label right after all modifications in dropdown has been done
- if (self.options.toggleLabel) {
- self.updateLabel(selectedObj, $el, self);
- }
+ // Update label right after all modifications in dropdown has been done
+ if (self.options.toggleLabel) {
+ self.updateLabel(selectedObj, $el, self);
+ }
- $el.trigger('blur');
- });
- }
+ $el.trigger('blur');
+ });
}
+ }
- // Finds an element inside wrapper element
- GitLabDropdown.prototype.getElement = function(selector) {
- return this.dropdown.find(selector);
- };
+ // Finds an element inside wrapper element
+ GitLabDropdown.prototype.getElement = function(selector) {
+ return this.dropdown.find(selector);
+ };
- GitLabDropdown.prototype.toggleLoading = function() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
- };
+ GitLabDropdown.prototype.toggleLoading = function() {
+ return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ };
- GitLabDropdown.prototype.togglePage = function() {
- var menu;
- menu = $('.dropdown-menu', this.dropdown);
- if (menu.hasClass(PAGE_TWO_CLASS)) {
- if (this.remote) {
- this.remote.execute();
- }
- }
- menu.toggleClass(PAGE_TWO_CLASS);
- // Focus first visible input on active page
- return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
- };
-
- GitLabDropdown.prototype.parseData = function(data) {
- var full_html, groupData, html, name;
- this.renderedData = data;
- if (this.options.filterable && data.length === 0) {
- // render no matching results
- html = [this.noResults()];
- } else {
- // Handle array groups
- if (gl.utils.isObject(data)) {
- html = [];
- for (name in data) {
- groupData = data[name];
- html.push(this.renderItem({
- header: name
- // Add header for each group
- }, name));
- this.renderData(groupData, name).map(function(item) {
- return html.push(item);
- });
- }
- } else {
- // Render each row
- html = this.renderData(data);
- }
- }
- // Render the full menu
- full_html = this.renderMenu(html);
- return this.appendMenu(full_html);
- };
-
- GitLabDropdown.prototype.renderData = function(data, group) {
- if (group == null) {
- group = false;
+ GitLabDropdown.prototype.togglePage = function() {
+ var menu;
+ menu = $('.dropdown-menu', this.dropdown);
+ if (menu.hasClass(PAGE_TWO_CLASS)) {
+ if (this.remote) {
+ this.remote.execute();
}
- return data.map((function(_this) {
- return function(obj, index) {
- return _this.renderItem(obj, group, index);
- };
- })(this));
- };
-
- GitLabDropdown.prototype.shouldPropagate = function(e) {
- var $target;
- if (this.options.multiSelect) {
- $target = $(e.target);
- if ($target && !$target.hasClass('dropdown-menu-close') &&
- !$target.hasClass('dropdown-menu-close-icon') &&
- !$target.data('is-link')) {
- e.stopPropagation();
- return false;
- } else {
- return true;
+ }
+ menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
+ return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+ };
+
+ GitLabDropdown.prototype.parseData = function(data) {
+ var full_html, groupData, html, name;
+ this.renderedData = data;
+ if (this.options.filterable && data.length === 0) {
+ // render no matching results
+ html = [this.noResults()];
+ } else {
+ // Handle array groups
+ if (gl.utils.isObject(data)) {
+ html = [];
+ for (name in data) {
+ groupData = data[name];
+ html.push(this.renderItem({
+ header: name
+ // Add header for each group
+ }, name));
+ this.renderData(groupData, name).map(function(item) {
+ return html.push(item);
+ });
}
+ } else {
+ // Render each row
+ html = this.renderData(data);
}
- };
+ }
+ // Render the full menu
+ full_html = this.renderMenu(html);
+ return this.appendMenu(full_html);
+ };
- GitLabDropdown.prototype.opened = function(e) {
- var contentHtml;
- this.resetRows();
- this.addArrowKeyEvent();
+ GitLabDropdown.prototype.renderData = function(data, group) {
+ if (group == null) {
+ group = false;
+ }
+ return data.map((function(_this) {
+ return function(obj, index) {
+ return _this.renderItem(obj, group, index);
+ };
+ })(this));
+ };
- // Makes indeterminate items effective
- if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- this.parseData(this.fullData);
- }
- contentHtml = $('.dropdown-content', this.dropdown).html();
- if (this.remote && contentHtml === "") {
- this.remote.execute();
+ GitLabDropdown.prototype.shouldPropagate = function(e) {
+ var $target;
+ if (this.options.multiSelect) {
+ $target = $(e.target);
+ if ($target && !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('is-link')) {
+ e.stopPropagation();
+ return false;
} else {
- this.focusTextInput();
+ return true;
}
+ }
+ };
- if (this.options.showMenuAbove) {
- this.positionMenuAbove();
- }
+ GitLabDropdown.prototype.opened = function(e) {
+ var contentHtml;
+ this.resetRows();
+ this.addArrowKeyEvent();
- if (this.options.opened) {
- this.options.opened.call(this, e);
- }
+ // Makes indeterminate items effective
+ if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
+ this.parseData(this.fullData);
+ }
+ contentHtml = $('.dropdown-content', this.dropdown).html();
+ if (this.remote && contentHtml === "") {
+ this.remote.execute();
+ } else {
+ this.focusTextInput();
+ }
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
- return this.dropdown.trigger('shown.gl.dropdown');
- };
+ if (this.options.opened) {
+ this.options.opened.call(this, e);
+ }
- GitLabDropdown.prototype.positionMenuAbove = function() {
- var $button = $(this.el);
- var $menu = this.dropdown.find('.dropdown-menu');
+ return this.dropdown.trigger('shown.gl.dropdown');
+ };
- $menu.css('top', ($button.height() + $menu.height()) * -1);
- };
+ GitLabDropdown.prototype.positionMenuAbove = function() {
+ var $button = $(this.el);
+ var $menu = this.dropdown.find('.dropdown-menu');
- GitLabDropdown.prototype.hidden = function(e) {
- var $input;
- this.resetRows();
- this.removeArrayKeyEvent();
- $input = this.dropdown.find(".dropdown-input-field");
- if (this.options.filterable) {
- $input.blur();
- }
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
- }
- if (this.options.hidden) {
- this.options.hidden.call(this, e);
- }
- return this.dropdown.trigger('hidden.gl.dropdown');
- };
+ $menu.css('top', ($button.height() + $menu.height()) * -1);
+ };
- // Render the full menu
- GitLabDropdown.prototype.renderMenu = function(html) {
- if (this.options.renderMenu) {
- return this.options.renderMenu(html);
- } else {
- var ul = document.createElement('ul');
+ GitLabDropdown.prototype.hidden = function(e) {
+ var $input;
+ this.resetRows();
+ this.removeArrayKeyEvent();
+ $input = this.dropdown.find(".dropdown-input-field");
+ if (this.options.filterable) {
+ $input.blur();
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+ }
+ if (this.options.hidden) {
+ this.options.hidden.call(this, e);
+ }
+ return this.dropdown.trigger('hidden.gl.dropdown');
+ };
- for (var i = 0; i < html.length; i += 1) {
- var el = html[i];
+ // Render the full menu
+ GitLabDropdown.prototype.renderMenu = function(html) {
+ if (this.options.renderMenu) {
+ return this.options.renderMenu(html);
+ } else {
+ var ul = document.createElement('ul');
- if (el instanceof jQuery) {
- el = el.get(0);
- }
+ for (var i = 0; i < html.length; i += 1) {
+ var el = html[i];
- if (typeof el === 'string') {
- ul.innerHTML += el;
- } else {
- ul.appendChild(el);
- }
+ if (el instanceof jQuery) {
+ el = el.get(0);
}
- return ul;
+ if (typeof el === 'string') {
+ ul.innerHTML += el;
+ } else {
+ ul.appendChild(el);
+ }
}
- };
- // Append the menu into the dropdown
- GitLabDropdown.prototype.appendMenu = function(html) {
- return this.clearMenu().append(html);
- };
+ return ul;
+ }
+ };
+
+ // Append the menu into the dropdown
+ GitLabDropdown.prototype.appendMenu = function(html) {
+ return this.clearMenu().append(html);
+ };
- GitLabDropdown.prototype.clearMenu = function() {
- var selector;
- selector = '.dropdown-content';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content";
- }
+ GitLabDropdown.prototype.clearMenu = function() {
+ var selector;
+ selector = '.dropdown-content';
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one .dropdown-content";
+ }
- return $(selector, this.dropdown).empty();
- };
+ return $(selector, this.dropdown).empty();
+ };
- GitLabDropdown.prototype.renderItem = function(data, group, index) {
- var field, fieldName, html, selected, text, url, value;
- if (group == null) {
- group = false;
+ GitLabDropdown.prototype.renderItem = function(data, group, index) {
+ var field, fieldName, html, selected, text, url, value;
+ if (group == null) {
+ group = false;
+ }
+ if (index == null) {
+ // Render the row
+ index = false;
+ }
+ html = document.createElement('li');
+ if (data === 'divider' || data === 'separator') {
+ html.className = data;
+ return html;
+ }
+ // Header
+ if (data.header != null) {
+ html.className = 'dropdown-header';
+ html.innerHTML = data.header;
+ return html;
+ }
+ if (this.options.renderRow) {
+ // Call the render function
+ html = this.options.renderRow.call(this.options, data, this);
+ } else {
+ if (!selected) {
+ value = this.options.id ? this.options.id(data) : data.id;
+ fieldName = this.options.fieldName;
+
+ if (value) { value = value.toString().replace(/'/g, '\\\''); }
+
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+ if (field.length) {
+ selected = true;
+ }
}
- if (index == null) {
- // Render the row
- index = false;
+ // Set URL
+ if (this.options.url != null) {
+ url = this.options.url(data);
+ } else {
+ url = data.url != null ? data.url : '#';
}
- html = document.createElement('li');
- if (data === 'divider' || data === 'separator') {
- html.className = data;
- return html;
+ // Set Text
+ if (this.options.text != null) {
+ text = this.options.text(data);
+ } else {
+ text = data.text != null ? data.text : '';
}
- // Header
- if (data.header != null) {
- html.className = 'dropdown-header';
- html.innerHTML = data.header;
- return html;
+ if (this.highlight) {
+ text = this.highlightTextMatches(text, this.filterInput.val());
}
- if (this.options.renderRow) {
- // Call the render function
- html = this.options.renderRow.call(this.options, data, this);
- } else {
- if (!selected) {
- value = this.options.id ? this.options.id(data) : data.id;
- fieldName = this.options.fieldName;
-
- if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
- if (field.length) {
- selected = true;
- }
- }
- // Set URL
- if (this.options.url != null) {
- url = this.options.url(data);
- } else {
- url = data.url != null ? data.url : '#';
- }
- // Set Text
- if (this.options.text != null) {
- text = this.options.text(data);
- } else {
- text = data.text != null ? data.text : '';
- }
- if (this.highlight) {
- text = this.highlightTextMatches(text, this.filterInput.val());
- }
- // Create the list item & the link
- var link = document.createElement('a');
-
- link.href = url;
- link.innerHTML = text;
+ // Create the list item & the link
+ var link = document.createElement('a');
- if (selected) {
- link.className = 'is-active';
- }
-
- if (group) {
- link.dataset.group = group;
- link.dataset.index = index;
- }
+ link.href = url;
+ link.innerHTML = text;
- html.appendChild(link);
+ if (selected) {
+ link.className = 'is-active';
}
- return html;
- };
-
- GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
- var occurrences;
- occurrences = fuzzaldrinPlus.match(text, term);
- return text.split('').map(function(character, i) {
- if (indexOf.call(occurrences, i) >= 0) {
- return "<b>" + character + "</b>";
- } else {
- return character;
- }
- }).join('');
- };
-
- GitLabDropdown.prototype.noResults = function() {
- var html;
- return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
- };
-
- GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
-
- fieldName = this.options.fieldName;
- isInput = $(this.el).is('input');
- if (this.renderedData) {
- groupName = el.data('group');
- if (groupName) {
- selectedIndex = el.data('index');
- selectedObject = this.renderedData[groupName][selectedIndex];
- } else {
- selectedIndex = el.closest('li').index();
- selectedObject = this.renderedData[selectedIndex];
- }
+
+ if (group) {
+ link.dataset.group = group;
+ link.dataset.index = index;
}
- if (this.options.vue) {
- if (el.hasClass(ACTIVE_CLASS)) {
- el.removeClass(ACTIVE_CLASS);
- } else {
- el.addClass(ACTIVE_CLASS);
- }
+ html.appendChild(link);
+ }
+ return html;
+ };
- return [selectedObject];
+ GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
+ var occurrences;
+ occurrences = fuzzaldrinPlus.match(text, term);
+ return text.split('').map(function(character, i) {
+ if (indexOf.call(occurrences, i) !== -1) {
+ return "<b>" + character + "</b>";
+ } else {
+ return character;
}
+ }).join('');
+ };
- field = [];
- value = this.options.id
- ? this.options.id(selectedObject, el)
- : selectedObject.id;
- if (isInput) {
- field = $(this.el);
- } else if (value) {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
- }
+ GitLabDropdown.prototype.noResults = function() {
+ var html;
+ return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+ };
+
+ GitLabDropdown.prototype.rowClicked = function(el) {
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
- if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return;
+ fieldName = this.options.fieldName;
+ isInput = $(this.el).is('input');
+ if (this.renderedData) {
+ groupName = el.data('group');
+ if (groupName) {
+ selectedIndex = el.data('index');
+ selectedObject = this.renderedData[groupName][selectedIndex];
+ } else {
+ selectedIndex = el.closest('li').index();
+ selectedObject = this.renderedData[selectedIndex];
}
+ }
+ if (this.options.vue) {
if (el.hasClass(ACTIVE_CLASS)) {
- isMarking = false;
el.removeClass(ACTIVE_CLASS);
- if (field && field.length) {
- this.clearField(field, isInput);
- }
- } else if (el.hasClass(INDETERMINATE_CLASS)) {
- isMarking = true;
- el.addClass(ACTIVE_CLASS);
- el.removeClass(INDETERMINATE_CLASS);
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- }
} else {
- isMarking = true;
- if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
- this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
- if (!isInput) {
- this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
- }
- }
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- // Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
- if (value != null) {
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- } else if (field && field.length) {
- field.val(value).trigger('change');
- }
- }
}
- return [selectedObject, isMarking];
- };
+ return [selectedObject];
+ }
- GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus(); }
- };
+ field = [];
+ value = this.options.id
+ ? this.options.id(selectedObject, el)
+ : selectedObject.id;
+ if (isInput) {
+ field = $(this.el);
+ } else if (value) {
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
+ }
- GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
- var $input;
- // Create hidden input for form
- $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
- if (this.options.inputId != null) {
- $input.attr('id', this.options.inputId);
- }
- return this.dropdown.before($input);
- };
-
- GitLabDropdown.prototype.selectRowAtIndex = function(index) {
- var $el, selector;
- // If we pass an option index
- if (typeof index !== "undefined") {
- selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
- } else {
- selector = ".dropdown-content .is-focused";
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
+ if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
+ el.removeClass(ACTIVE_CLASS);
+ if (field && field.length) {
+ this.clearField(field, isInput);
+ }
+ } else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
+ el.addClass(ACTIVE_CLASS);
+ el.removeClass(INDETERMINATE_CLASS);
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ }
+ } else {
+ isMarking = true;
+ if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
+ this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
+ if (!isInput) {
+ this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
+ }
}
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
}
- // simulate a click on the first link
- $el = $(selector, this.dropdown);
- if ($el.length) {
- var href = $el.attr('href');
- if (href && href !== '#') {
- gl.utils.visitUrl(href);
- } else {
- $el.first().trigger('click');
+ // Toggle active class for the tick mark
+ el.addClass(ACTIVE_CLASS);
+ if (value != null) {
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ } else if (field && field.length) {
+ field.val(value).trigger('change');
}
}
- };
+ }
- GitLabDropdown.prototype.addArrowKeyEvent = function() {
- var $input, ARROW_KEY_CODES, selector;
- ARROW_KEY_CODES = [38, 40];
- $input = this.dropdown.find(".dropdown-input-field");
- selector = SELECTABLE_CLASSES;
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
+ return [selectedObject, isMarking];
+ };
+
+ GitLabDropdown.prototype.focusTextInput = function() {
+ if (this.options.filterable) { this.filterInput.focus(); }
+ };
+
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+ var $input;
+ // Create hidden input for form
+ $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
+ if (this.options.inputId != null) {
+ $input.attr('id', this.options.inputId);
+ }
+ return this.dropdown.before($input);
+ };
+
+ GitLabDropdown.prototype.selectRowAtIndex = function(index) {
+ var $el, selector;
+ // If we pass an option index
+ if (typeof index !== "undefined") {
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+ } else {
+ selector = ".dropdown-content .is-focused";
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ // simulate a click on the first link
+ $el = $(selector, this.dropdown);
+ if ($el.length) {
+ var href = $el.attr('href');
+ if (href && href !== '#') {
+ gl.utils.visitUrl(href);
+ } else {
+ $el.first().trigger('click');
}
- return $('body').on('keydown', (function(_this) {
- return function(e) {
- var $listItems, PREV_INDEX, currentKeyCode;
- currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) {
- e.preventDefault();
- e.stopImmediatePropagation();
- PREV_INDEX = currentIndex;
- $listItems = $(selector, _this.dropdown);
- // if @options.filterable
- // $input.blur()
- if (currentKeyCode === 40) {
- // Move down
- if (currentIndex < ($listItems.length - 1)) {
- currentIndex += 1;
- }
- } else if (currentKeyCode === 38) {
- // Move up
- if (currentIndex > 0) {
- currentIndex -= 1;
- }
+ }
+ };
+
+ GitLabDropdown.prototype.addArrowKeyEvent = function() {
+ var $input, ARROW_KEY_CODES, selector;
+ ARROW_KEY_CODES = [38, 40];
+ $input = this.dropdown.find(".dropdown-input-field");
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $('body').on('keydown', (function(_this) {
+ return function(e) {
+ var $listItems, PREV_INDEX, currentKeyCode;
+ currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, _this.dropdown);
+ // if @options.filterable
+ // $input.blur()
+ if (currentKeyCode === 40) {
+ // Move down
+ if (currentIndex < ($listItems.length - 1)) {
+ currentIndex += 1;
}
- if (currentIndex !== PREV_INDEX) {
- _this.highlightRowAtIndex($listItems, currentIndex);
+ } else if (currentKeyCode === 38) {
+ // Move up
+ if (currentIndex > 0) {
+ currentIndex -= 1;
}
- return false;
}
- if (currentKeyCode === 13 && currentIndex !== -1) {
- e.preventDefault();
- _this.selectRowAtIndex();
+ if (currentIndex !== PREV_INDEX) {
+ _this.highlightRowAtIndex($listItems, currentIndex);
}
- };
- })(this));
- };
-
- GitLabDropdown.prototype.removeArrayKeyEvent = function() {
- return $('body').off('keydown');
- };
-
- GitLabDropdown.prototype.resetRows = function resetRows() {
- currentIndex = -1;
- $('.is-focused', this.dropdown).removeClass('is-focused');
- };
-
- GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
- var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
- // Remove the class for the previously focused row
- $('.is-focused', this.dropdown).removeClass('is-focused');
- // Update the class for the row at the specific index
- $listItem = $listItems.eq(index);
- $listItem.find('a:first-child').addClass("is-focused");
- // Dropdown content scroll area
- $dropdownContent = $listItem.closest('.dropdown-content');
- dropdownScrollTop = $dropdownContent.scrollTop();
- dropdownContentHeight = $dropdownContent.outerHeight();
- dropdownContentTop = $dropdownContent.prop('offsetTop');
- dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
- // Get the offset bottom of the list item
- listItemHeight = $listItem.outerHeight();
- listItemTop = $listItem.prop('offsetTop');
- listItemBottom = listItemTop + listItemHeight;
- if (!index) {
- // Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0);
- } else if (index === ($listItems.length - 1)) {
- // Scroll the dropdown content to the bottom
- $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
- } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
- // Scroll the dropdown content down
- $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
- } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
- // Scroll the dropdown content up
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
- }
- };
+ return false;
+ }
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
+ _this.selectRowAtIndex();
+ }
+ };
+ })(this));
+ };
- GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
- if (selected == null) {
- selected = null;
- }
- if (el == null) {
- el = null;
- }
- if (instance == null) {
- instance = null;
- }
- return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
- };
+ GitLabDropdown.prototype.removeArrayKeyEvent = function() {
+ return $('body').off('keydown');
+ };
- GitLabDropdown.prototype.clearField = function(field, isInput) {
- return isInput ? field.val('') : field.remove();
- };
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
- return GitLabDropdown;
- })();
+ GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
+ var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ // Remove the class for the previously focused row
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
+ $listItem = $listItems.eq(index);
+ $listItem.find('a:first-child').addClass("is-focused");
+ // Dropdown content scroll area
+ $dropdownContent = $listItem.closest('.dropdown-content');
+ dropdownScrollTop = $dropdownContent.scrollTop();
+ dropdownContentHeight = $dropdownContent.outerHeight();
+ dropdownContentTop = $dropdownContent.prop('offsetTop');
+ dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
+ listItemHeight = $listItem.outerHeight();
+ listItemTop = $listItem.prop('offsetTop');
+ listItemBottom = listItemTop + listItemHeight;
+ if (!index) {
+ // Scroll the dropdown content to the top
+ $dropdownContent.scrollTop(0);
+ } else if (index === ($listItems.length - 1)) {
+ // Scroll the dropdown content to the bottom
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ // Scroll the dropdown content down
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ // Scroll the dropdown content up
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
+ }
+ };
- $.fn.glDropdown = function(opts) {
- return this.each(function() {
- if (!$.data(this, 'glDropdown')) {
- return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
- }
- });
+ GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
+ if (selected == null) {
+ selected = null;
+ }
+ if (el == null) {
+ el = null;
+ }
+ if (instance == null) {
+ instance = null;
+ }
+ return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
+ };
+
+ GitLabDropdown.prototype.clearField = function(field, isInput) {
+ return isInput ? field.val('') : field.remove();
};
-}).call(window);
+
+ return GitLabDropdown;
+})();
+
+$.fn.glDropdown = function(opts) {
+ return this.each(function() {
+ if (!$.data(this, 'glDropdown')) {
+ return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
+ }
+ });
+};
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
new file mode 100644
index 00000000000..76de249ac3b
--- /dev/null
+++ b/app/assets/javascripts/gl_field_error.js
@@ -0,0 +1,162 @@
+/**
+ * This class overrides the browser's validation error bubbles, displaying custom
+ * error messages for invalid fields instead. To begin validating any form, add the
+ * class `gl-show-field-errors` to the form element, and ensure error messages are
+ * declared in each inputs' `title` attribute. If no title is declared for an invalid
+ * field the user attempts to submit, "This field is required." will be shown by default.
+ *
+ * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
+ *
+ * Set a custom error anchor for error message to be injected after with the
+ * class `gl-field-error-anchor`
+ *
+ * Examples:
+ *
+ * Basic:
+ *
+ * <form class='gl-show-field-errors'>
+ * <input type='text' name='username' title='Username is required.'/>
+ * </form>
+ *
+ * Ignore specific inputs (e.g. UsernameValidator):
+ *
+ * <form class='gl-show-field-errors'>
+ * <div class="form-group>
+ * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
+ * </div>
+ * <div class="form-group">
+ * <input type='text' name='username' title='Username is required.'/>
+ * </div>
+ * </form>
+ *
+ * Custom Error Anchor (allows error message to be injected after specified element):
+ *
+ * <form class='gl-show-field-errors'>
+ * <div class="form-group gl-field-error-anchor">
+ * <input type='text' name='username' title='Username is required.'/>
+ * // Error message typically injected here
+ * </div>
+ * // Error message now injected here
+ * </form>
+ *
+ */
+
+/**
+ * Regex Patterns in use:
+ *
+ * Only alphanumeric: : "[a-zA-Z0-9]+"
+ * No special characters : "[a-zA-Z0-9-_]+",
+ *
+ */
+
+const errorMessageClass = 'gl-field-error';
+const inputErrorClass = 'gl-field-error-outline';
+const errorAnchorSelector = '.gl-field-error-anchor';
+const ignoreInputSelector = '.gl-field-error-ignore';
+
+class GlFieldError {
+ constructor({ input, formErrors }) {
+ this.inputElement = $(input);
+ this.inputDomElement = this.inputElement.get(0);
+ this.form = formErrors;
+ this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+
+ this.state = {
+ valid: false,
+ empty: true,
+ };
+
+ this.initFieldValidation();
+ }
+
+ initFieldValidation() {
+ const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
+ const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
+
+ // hidden when injected into DOM
+ errorAnchor.after(this.fieldErrorElement);
+ this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
+ this.scopedSiblings = this.safelySelectSiblings();
+ }
+
+ safelySelectSiblings() {
+ // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
+ const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
+ const parentContainer = this.inputElement.parent('.form-group');
+
+ // Only select siblings when they're scoped within a form-group with one input
+ const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
+
+ return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
+ }
+
+ renderValidity() {
+ this.renderClear();
+
+ if (this.state.valid) {
+ this.renderValid();
+ } else if (this.state.empty) {
+ this.renderEmpty();
+ } else if (!this.state.valid) {
+ this.renderInvalid();
+ }
+ }
+
+ handleInvalidSubmit(event) {
+ event.preventDefault();
+ const currentValue = this.accessCurrentValue();
+ this.state.valid = false;
+ this.state.empty = currentValue === '';
+
+ this.renderValidity();
+ this.form.focusOnFirstInvalid.apply(this.form);
+ // For UX, wait til after first invalid submission to check each keyup
+ this.inputElement.off('keyup.fieldValidator')
+ .on('keyup.fieldValidator', this.updateValidity.bind(this));
+ }
+
+ /* Get or set current input value */
+ accessCurrentValue(newVal) {
+ return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
+ }
+
+ getInputValidity() {
+ return this.inputDomElement.validity.valid;
+ }
+
+ updateValidity() {
+ const inputVal = this.accessCurrentValue();
+ this.state.empty = !inputVal.length;
+ this.state.valid = this.getInputValidity();
+ this.renderValidity();
+ }
+
+ renderValid() {
+ return this.renderClear();
+ }
+
+ renderEmpty() {
+ return this.renderInvalid();
+ }
+
+ renderInvalid() {
+ this.inputElement.addClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ return this.fieldErrorElement.show();
+ }
+
+ renderClear() {
+ const inputVal = this.accessCurrentValue();
+ if (!inputVal.split(' ').length) {
+ const trimmedInput = inputVal.trim();
+ this.accessCurrentValue(trimmedInput);
+ }
+ this.inputElement.removeClass(inputErrorClass);
+ this.scopedSiblings.hide();
+ this.fieldErrorElement.hide();
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.GlFieldError = GlFieldError;
diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6
deleted file mode 100644
index f7cbecc0385..00000000000
--- a/app/assets/javascripts/gl_field_error.js.es6
+++ /dev/null
@@ -1,164 +0,0 @@
-/* eslint-disable no-param-reassign */
-((global) => {
- /*
- * This class overrides the browser's validation error bubbles, displaying custom
- * error messages for invalid fields instead. To begin validating any form, add the
- * class `gl-show-field-errors` to the form element, and ensure error messages are
- * declared in each inputs' `title` attribute. If no title is declared for an invalid
- * field the user attempts to submit, "This field is required." will be shown by default.
- *
- * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
- *
- * Set a custom error anchor for error message to be injected after with the
- * class `gl-field-error-anchor`
- *
- * Examples:
- *
- * Basic:
- *
- * <form class='gl-show-field-errors'>
- * <input type='text' name='username' title='Username is required.'/>
- * </form>
- *
- * Ignore specific inputs (e.g. UsernameValidator):
- *
- * <form class='gl-show-field-errors'>
- * <div class="form-group>
- * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
- * </div>
- * <div class="form-group">
- * <input type='text' name='username' title='Username is required.'/>
- * </div>
- * </form>
- *
- * Custom Error Anchor (allows error message to be injected after specified element):
- *
- * <form class='gl-show-field-errors'>
- * <div class="form-group gl-field-error-anchor">
- * <input type='text' name='username' title='Username is required.'/>
- * // Error message typically injected here
- * </div>
- * // Error message now injected here
- * </form>
- *
- * */
-
- /*
- * Regex Patterns in use:
- *
- * Only alphanumeric: : "[a-zA-Z0-9]+"
- * No special characters : "[a-zA-Z0-9-_]+",
- *
- * */
-
- const errorMessageClass = 'gl-field-error';
- const inputErrorClass = 'gl-field-error-outline';
- const errorAnchorSelector = '.gl-field-error-anchor';
- const ignoreInputSelector = '.gl-field-error-ignore';
-
- class GlFieldError {
- constructor({ input, formErrors }) {
- this.inputElement = $(input);
- this.inputDomElement = this.inputElement.get(0);
- this.form = formErrors;
- this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
- this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
-
- this.state = {
- valid: false,
- empty: true,
- };
-
- this.initFieldValidation();
- }
-
- initFieldValidation() {
- const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
- const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
-
- // hidden when injected into DOM
- errorAnchor.after(this.fieldErrorElement);
- this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
- this.scopedSiblings = this.safelySelectSiblings();
- }
-
- safelySelectSiblings() {
- // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
- const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
- const parentContainer = this.inputElement.parent('.form-group');
-
- // Only select siblings when they're scoped within a form-group with one input
- const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
-
- return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
- }
-
- renderValidity() {
- this.renderClear();
-
- if (this.state.valid) {
- this.renderValid();
- } else if (this.state.empty) {
- this.renderEmpty();
- } else if (!this.state.valid) {
- this.renderInvalid();
- }
- }
-
- handleInvalidSubmit(event) {
- event.preventDefault();
- const currentValue = this.accessCurrentValue();
- this.state.valid = false;
- this.state.empty = currentValue === '';
-
- this.renderValidity();
- this.form.focusOnFirstInvalid.apply(this.form);
- // For UX, wait til after first invalid submission to check each keyup
- this.inputElement.off('keyup.fieldValidator')
- .on('keyup.fieldValidator', this.updateValidity.bind(this));
- }
-
- /* Get or set current input value */
- accessCurrentValue(newVal) {
- return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
- }
-
- getInputValidity() {
- return this.inputDomElement.validity.valid;
- }
-
- updateValidity() {
- const inputVal = this.accessCurrentValue();
- this.state.empty = !inputVal.length;
- this.state.valid = this.getInputValidity();
- this.renderValidity();
- }
-
- renderValid() {
- return this.renderClear();
- }
-
- renderEmpty() {
- return this.renderInvalid();
- }
-
- renderInvalid() {
- this.inputElement.addClass(inputErrorClass);
- this.scopedSiblings.hide();
- return this.fieldErrorElement.show();
- }
-
- renderClear() {
- const inputVal = this.accessCurrentValue();
- if (!inputVal.split(' ').length) {
- const trimmedInput = inputVal.trim();
- this.accessCurrentValue(trimmedInput);
- }
- this.inputElement.removeClass(inputErrorClass);
- this.scopedSiblings.hide();
- this.fieldErrorElement.hide();
- }
- }
-
- global.GlFieldError = GlFieldError;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
new file mode 100644
index 00000000000..636258ec555
--- /dev/null
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -0,0 +1,47 @@
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
+
+require('./gl_field_error');
+
+const customValidationFlag = 'gl-field-error-ignore';
+
+class GlFieldErrors {
+ constructor(form) {
+ this.form = $(form);
+ this.state = {
+ inputs: [],
+ valid: false
+ };
+ this.initValidators();
+ }
+
+ initValidators () {
+ // register selectors here as needed
+ const validateSelectors = [':text', ':password', '[type=email]']
+ .map((selector) => `input${selector}`).join(',');
+
+ this.state.inputs = this.form.find(validateSelectors).toArray()
+ .filter((input) => !input.classList.contains(customValidationFlag))
+ .map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
+
+ this.form.on('submit', this.catchInvalidFormSubmit);
+ }
+
+ /* Neccessary to prevent intercept and override invalid form submit
+ * because Safari & iOS quietly allow form submission when form is invalid
+ * and prevents disabling of invalid submit button by application.js */
+
+ catchInvalidFormSubmit (event) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ focusOnFirstInvalid () {
+ const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ firstInvalid.inputElement.focus();
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.GlFieldErrors = GlFieldErrors;
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
deleted file mode 100644
index e9add115429..00000000000
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-
-require('./gl_field_error');
-
-((global) => {
- const customValidationFlag = 'gl-field-error-ignore';
-
- class GlFieldErrors {
- constructor(form) {
- this.form = $(form);
- this.state = {
- inputs: [],
- valid: false
- };
- this.initValidators();
- }
-
- initValidators () {
- // register selectors here as needed
- const validateSelectors = [':text', ':password', '[type=email]']
- .map((selector) => `input${selector}`).join(',');
-
- this.state.inputs = this.form.find(validateSelectors).toArray()
- .filter((input) => !input.classList.contains(customValidationFlag))
- .map((input) => new global.GlFieldError({ input, formErrors: this }));
-
- this.form.on('submit', this.catchInvalidFormSubmit);
- }
-
- /* Neccessary to prevent intercept and override invalid form submit
- * because Safari & iOS quietly allow form submission when form is invalid
- * and prevents disabling of invalid submit button by application.js */
-
- catchInvalidFormSubmit (event) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
- }
- }
-
- focusOnFirstInvalid () {
- const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
- firstInvalid.inputElement.focus();
- }
- }
-
- global.GlFieldErrors = GlFieldErrors;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
new file mode 100644
index 00000000000..e7c98e16581
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js
@@ -0,0 +1,90 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
+window.gl = window.gl || {};
+
+function GLForm(form) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
+}
+
+GLForm.prototype.destroy = function() {
+ // Clean form listeners
+ this.clearEventListeners();
+ return this.form.data('gl-form', null);
+};
+
+GLForm.prototype.setupForm = function() {
+ var isNewForm;
+ isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new DropzoneInput(this.form);
+ autosize(this.textarea);
+ // form and textarea event listeners
+ this.addEventListeners();
+ }
+ gl.text.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+};
+
+GLForm.prototype.setupAutosize = function () {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
+
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
+
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+};
+
+GLForm.prototype.setHeightData = function () {
+ this.textarea.data('height', this.textarea.outerHeight());
+};
+
+GLForm.prototype.destroyAutosize = function () {
+ const outerHeight = this.textarea.outerHeight();
+
+ if (this.textarea.data('height') === outerHeight) return;
+
+ autosize.destroy(this.textarea);
+
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+};
+
+GLForm.prototype.clearEventListeners = function() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ return gl.text.removeListeners(this.form);
+};
+
+GLForm.prototype.addEventListeners = function() {
+ this.textarea.on('focus', function() {
+ return $(this).closest('.md-area').addClass('is-focused');
+ });
+ return this.textarea.on('blur', function() {
+ return $(this).closest('.md-area').removeClass('is-focused');
+ });
+};
+
+window.gl.GLForm = GLForm;
diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6
deleted file mode 100644
index 0b446ff364a..00000000000
--- a/app/assets/javascripts/gl_form.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
-/* global GitLab */
-/* global DropzoneInput */
-/* global autosize */
-
-(() => {
- const global = window.gl || (window.gl = {});
-
- function GLForm(form) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
- }
-
- GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- return this.form.data('gl-form', null);
- };
-
- GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
- new DropzoneInput(this.form);
- autosize(this.textarea);
- // form and textarea event listeners
- this.addEventListeners();
- }
- gl.text.init(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
- this.form.show();
- if (this.isAutosizeable) this.setupAutosize();
- };
-
- GLForm.prototype.setupAutosize = function () {
- this.textarea.off('autosize:resized')
- .on('autosize:resized', this.setHeightData.bind(this));
-
- this.textarea.off('mouseup.autosize')
- .on('mouseup.autosize', this.destroyAutosize.bind(this));
-
- setTimeout(() => {
- autosize(this.textarea);
- this.textarea.css('resize', 'vertical');
- }, 0);
- };
-
- GLForm.prototype.setHeightData = function () {
- this.textarea.data('height', this.textarea.outerHeight());
- };
-
- GLForm.prototype.destroyAutosize = function () {
- const outerHeight = this.textarea.outerHeight();
-
- if (this.textarea.data('height') === outerHeight) return;
-
- autosize.destroy(this.textarea);
-
- this.textarea.data('height', outerHeight);
- this.textarea.outerHeight(outerHeight);
- this.textarea.css('max-height', window.outerHeight);
- };
-
- GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
- };
-
- GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
- };
-
- global.GLForm = GLForm;
-})();
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 4f7777aa5bc..a433c7ba8f0 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,3 +1,6 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
+import Chart from 'vendor/Chart';
+import ContributorsStatGraph from './stat_graph_contributors';
+
+// export to global scope
+window.Chart = Chart;
+window.ContributorsStatGraph = ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
deleted file mode 100644
index 75a53aae33c..00000000000
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
-(function() {
- this.StatGraph = (function() {
- function StatGraph() {}
-
- StatGraph.log = {};
-
- StatGraph.get_log = function() {
- return this.log;
- };
-
- StatGraph.set_log = function(data) {
- return this.log = data;
- };
-
- return StatGraph;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index bbfb467ad50..c6be4c9e8fe 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,116 +1,111 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
-/* global ContributorsGraph */
-/* global ContributorsAuthorGraph */
-/* global ContributorsMasterGraph */
-/* global ContributorsStatGraphUtil */
-/* global d3 */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */
-window.d3 = require('d3');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
+import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-(function() {
- this.ContributorsStatGraph = (function() {
- function ContributorsStatGraph() {}
+export default (function() {
+ function ContributorsStatGraph() {}
- ContributorsStatGraph.prototype.init = function(log) {
- var author_commits, total_commits;
- this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field("commits");
- total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
- this.add_master_graph(total_commits);
- this.add_authors_graph(author_commits);
- return this.change_date_header();
- };
+ ContributorsStatGraph.prototype.init = function(log) {
+ var author_commits, total_commits;
+ this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
+ this.set_current_field("commits");
+ total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
+ this.add_master_graph(total_commits);
+ this.add_authors_graph(author_commits);
+ return this.change_date_header();
+ };
- ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
- this.master_graph = new ContributorsMasterGraph(total_data);
- return this.master_graph.draw();
- };
+ ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
+ this.master_graph = new ContributorsMasterGraph(total_data);
+ return this.master_graph.draw();
+ };
- ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
- var limited_author_data;
- this.authors = [];
- limited_author_data = author_data.slice(0, 100);
- return _.each(limited_author_data, (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $(".contributors-list").append(author_header);
- _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
- return author_graph.draw();
- };
- })(this));
- };
+ ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
+ var limited_author_data;
+ this.authors = [];
+ limited_author_data = author_data.slice(0, 100);
+ return _.each(limited_author_data, (function(_this) {
+ return function(d) {
+ var author_graph, author_header;
+ author_header = _this.create_author_header(d);
+ $(".contributors-list").append(author_header);
+ _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+ return author_graph.draw();
+ };
+ })(this));
+ };
- ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
- var commits;
- commits = $('<span/>', {
- "class": 'graph-author-commits-count'
- });
- commits.text(author.commits + " commits");
- return $('<span/>').append(commits);
- };
+ ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
+ var commits;
+ commits = $('<span/>', {
+ "class": 'graph-author-commits-count'
+ });
+ commits.text(author.commits + " commits");
+ return $('<span/>').append(commits);
+ };
- ContributorsStatGraph.prototype.create_author_header = function(author) {
- var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
- list_item = $('<li/>', {
- "class": 'person',
- style: 'display: block;'
- });
- author_name = $('<h4>' + author.author_name + '</h4>');
- author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
- author_commit_info_span = $('<span/>', {
- "class": 'commits'
- });
- author_commit_info = this.format_author_commit_info(author);
- author_commit_info_span.html(author_commit_info);
- list_item.append(author_name);
- list_item.append(author_email);
- list_item.append(author_commit_info_span);
- return list_item;
- };
+ ContributorsStatGraph.prototype.create_author_header = function(author) {
+ var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
+ list_item = $('<li/>', {
+ "class": 'person',
+ style: 'display: block;'
+ });
+ author_name = $('<h4>' + author.author_name + '</h4>');
+ author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
+ author_commit_info_span = $('<span/>', {
+ "class": 'commits'
+ });
+ author_commit_info = this.format_author_commit_info(author);
+ author_commit_info_span.html(author_commit_info);
+ list_item.append(author_name);
+ list_item.append(author_email);
+ list_item.append(author_commit_info_span);
+ return list_item;
+ };
- ContributorsStatGraph.prototype.redraw_master = function() {
- var total_data;
- total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- this.master_graph.set_data(total_data);
- return this.master_graph.redraw();
- };
+ ContributorsStatGraph.prototype.redraw_master = function() {
+ var total_data;
+ total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ this.master_graph.set_data(total_data);
+ return this.master_graph.redraw();
+ };
- ContributorsStatGraph.prototype.redraw_authors = function() {
- var author_commits, x_domain;
- $("ol").html("");
- x_domain = ContributorsGraph.prototype.x_domain;
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
- return _.each(author_commits, (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- };
- })(this));
- };
+ ContributorsStatGraph.prototype.redraw_authors = function() {
+ var author_commits, x_domain;
+ $("ol").html("");
+ x_domain = ContributorsGraph.prototype.x_domain;
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+ return _.each(author_commits, (function(_this) {
+ return function(d) {
+ _this.redraw_author_commit_info(d);
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ };
+ })(this));
+ };
- ContributorsStatGraph.prototype.set_current_field = function(field) {
- return this.field = field;
- };
+ ContributorsStatGraph.prototype.set_current_field = function(field) {
+ return this.field = field;
+ };
- ContributorsStatGraph.prototype.change_date_header = function() {
- var print, print_date_format, x_domain;
- x_domain = ContributorsGraph.prototype.x_domain;
- print_date_format = d3.time.format("%B %e %Y");
- print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
- return $("#date_header").text(print);
- };
+ ContributorsStatGraph.prototype.change_date_header = function() {
+ var print, print_date_format, x_domain;
+ x_domain = ContributorsGraph.prototype.x_domain;
+ print_date_format = d3.time.format("%B %e %Y");
+ print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
+ return $("#date_header").text(print);
+ };
- ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item;
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
- };
+ ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
+ var author_commit_info, author_list_item;
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ };
- return ContributorsStatGraph;
- })();
-}).call(window);
+ return ContributorsStatGraph;
+})();
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 228771da4ee..521bc77db66 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,276 +1,272 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */
-/* global d3 */
-/* global ContributorsGraph */
-
-window.d3 = require('d3');
-
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
- extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ContributorsGraph = (function() {
- function ContributorsGraph() {}
-
- ContributorsGraph.prototype.MARGIN = {
- top: 20,
- right: 20,
- bottom: 30,
- left: 50
- };
-
- ContributorsGraph.prototype.x_domain = null;
-
- ContributorsGraph.prototype.y_domain = null;
-
- ContributorsGraph.prototype.dates = [];
-
- ContributorsGraph.set_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = data;
- };
-
- ContributorsGraph.set_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
- };
-
- ContributorsGraph.init_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
- return d.date;
- });
- };
-
- ContributorsGraph.init_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
- };
-
- ContributorsGraph.init_domain = function(data) {
- ContributorsGraph.init_x_domain(data);
- return ContributorsGraph.init_y_domain(data);
- };
-
- ContributorsGraph.set_dates = function(data) {
- return ContributorsGraph.prototype.dates = data;
- };
-
- ContributorsGraph.prototype.set_x_domain = function() {
- return this.x.domain(this.x_domain);
- };
-
- ContributorsGraph.prototype.set_y_domain = function() {
- return this.y.domain(this.y_domain);
- };
-
- ContributorsGraph.prototype.set_domain = function() {
- this.set_x_domain();
- return this.set_y_domain();
- };
-
- ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.time.scale().range([0, width]).clamp(true);
- return this.y = d3.scale.linear().range([height, 0]).nice();
- };
-
- ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
- };
-
- ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
- };
-
- ContributorsGraph.prototype.set_data = function(data) {
- return this.data = data;
- };
-
- return ContributorsGraph;
- })();
-
- this.ContributorsMasterGraph = (function(superClass) {
- extend(ContributorsMasterGraph, superClass);
-
- function ContributorsMasterGraph(data1) {
- this.data = data1;
- this.update_content = bind(this.update_content, this);
- this.width = $('.content').width() - 70;
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.brush = null;
- this.x_max_domain = null;
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+
+import d3 from 'd3';
+
+const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+const hasProp = {}.hasOwnProperty;
+
+export const ContributorsGraph = (function() {
+ function ContributorsGraph() {}
+
+ ContributorsGraph.prototype.MARGIN = {
+ top: 20,
+ right: 20,
+ bottom: 30,
+ left: 50
+ };
+
+ ContributorsGraph.prototype.x_domain = null;
+
+ ContributorsGraph.prototype.y_domain = null;
+
+ ContributorsGraph.prototype.dates = [];
+
+ ContributorsGraph.set_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = data;
+ };
+
+ ContributorsGraph.set_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ return d.commits = d.commits || d.additions || d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+ return d.date;
+ });
+ };
+
+ ContributorsGraph.init_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ return d.commits = d.commits || d.additions || d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_domain = function(data) {
+ ContributorsGraph.init_x_domain(data);
+ return ContributorsGraph.init_y_domain(data);
+ };
+
+ ContributorsGraph.set_dates = function(data) {
+ return ContributorsGraph.prototype.dates = data;
+ };
+
+ ContributorsGraph.prototype.set_x_domain = function() {
+ return this.x.domain(this.x_domain);
+ };
+
+ ContributorsGraph.prototype.set_y_domain = function() {
+ return this.y.domain(this.y_domain);
+ };
+
+ ContributorsGraph.prototype.set_domain = function() {
+ this.set_x_domain();
+ return this.set_y_domain();
+ };
+
+ ContributorsGraph.prototype.create_scale = function(width, height) {
+ this.x = d3.time.scale().range([0, width]).clamp(true);
+ return this.y = d3.scale.linear().range([height, 0]).nice();
+ };
+
+ ContributorsGraph.prototype.draw_x_axis = function() {
+ return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+ };
+
+ ContributorsGraph.prototype.draw_y_axis = function() {
+ return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+ };
+
+ ContributorsGraph.prototype.set_data = function(data) {
+ return this.data = data;
+ };
+
+ return ContributorsGraph;
+})();
+
+export const ContributorsMasterGraph = (function(superClass) {
+ extend(ContributorsMasterGraph, superClass);
+
+ function ContributorsMasterGraph(data1) {
+ this.data = data1;
+ this.update_content = bind(this.update_content, this);
+ this.width = $('.content').width() - 70;
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.brush = null;
+ this.x_max_domain = null;
+ }
+
+ ContributorsMasterGraph.prototype.process_dates = function(data) {
+ var dates;
+ dates = this.get_dates(data);
+ this.parse_dates(data);
+ return ContributorsGraph.set_dates(dates);
+ };
+
+ ContributorsMasterGraph.prototype.get_dates = function(data) {
+ return _.pluck(data, 'date');
+ };
+
+ ContributorsMasterGraph.prototype.parse_dates = function(data) {
+ var parseDate;
+ parseDate = d3.time.format("%Y-%m-%d").parse;
+ return data.forEach(function(d) {
+ return d.date = parseDate(d.date);
+ });
+ };
+
+ ContributorsMasterGraph.prototype.create_scale = function() {
+ return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsMasterGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsMasterGraph.prototype.create_svg = function() {
+ return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsMasterGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
+ return x(d.date);
+ }).y0(this.height).y1(function(d) {
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
+ }).interpolate("basis");
+ };
+
+ ContributorsMasterGraph.prototype.create_brush = function() {
+ return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ };
+
+ ContributorsMasterGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+ };
+
+ ContributorsMasterGraph.prototype.add_brush = function() {
+ return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+ };
+
+ ContributorsMasterGraph.prototype.update_content = function() {
+ ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ return $("#brush_change").trigger('change');
+ };
+
+ ContributorsMasterGraph.prototype.draw = function() {
+ this.process_dates(this.data);
+ this.create_scale();
+ this.create_axes();
+ ContributorsGraph.init_domain(this.data);
+ this.x_max_domain = this.x_domain;
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.create_brush();
+ this.draw_path(this.data);
+ this.draw_x_axis();
+ this.draw_y_axis();
+ return this.add_brush();
+ };
+
+ ContributorsMasterGraph.prototype.redraw = function() {
+ this.process_dates(this.data);
+ ContributorsGraph.set_y_domain(this.data);
+ this.set_y_domain();
+ this.svg.select("path").datum(this.data);
+ this.svg.select("path").attr("d", this.area);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsMasterGraph;
+})(ContributorsGraph);
+
+export const ContributorsAuthorGraph = (function(superClass) {
+ extend(ContributorsAuthorGraph, superClass);
+
+ function ContributorsAuthorGraph(data1) {
+ this.data = data1;
+ // Don't split graph size in half for mobile devices.
+ if ($(window).width() < 768) {
+ this.width = $('.content').width() - 80;
+ } else {
+ this.width = ($('.content').width() / 2) - 100;
}
-
- ContributorsMasterGraph.prototype.process_dates = function(data) {
- var dates;
- dates = this.get_dates(data);
- this.parse_dates(data);
- return ContributorsGraph.set_dates(dates);
- };
-
- ContributorsMasterGraph.prototype.get_dates = function(data) {
- return _.pluck(data, 'date');
- };
-
- ContributorsMasterGraph.prototype.parse_dates = function(data) {
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.list_item = null;
+ }
+
+ ContributorsAuthorGraph.prototype.create_scale = function() {
+ return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsAuthorGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsAuthorGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
var parseDate;
parseDate = d3.time.format("%Y-%m-%d").parse;
- return data.forEach(function(d) {
- return d.date = parseDate(d.date);
- });
- };
-
- ContributorsMasterGraph.prototype.create_scale = function() {
- return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
- };
-
- ContributorsMasterGraph.prototype.create_svg = function() {
- return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
- };
-
- ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
- return x(d.date);
- }).y0(this.height).y1(function(d) {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- }).interpolate("basis");
- };
-
- ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
- };
-
- ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
- };
-
- ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
- };
-
- ContributorsMasterGraph.prototype.update_content = function() {
- ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
- return $("#brush_change").trigger('change');
- };
-
- ContributorsMasterGraph.prototype.draw = function() {
- this.process_dates(this.data);
- this.create_scale();
- this.create_axes();
- ContributorsGraph.init_domain(this.data);
- this.x_max_domain = this.x_domain;
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.create_brush();
- this.draw_path(this.data);
- this.draw_x_axis();
- this.draw_y_axis();
- return this.add_brush();
- };
-
- ContributorsMasterGraph.prototype.redraw = function() {
- this.process_dates(this.data);
- ContributorsGraph.set_y_domain(this.data);
- this.set_y_domain();
- this.svg.select("path").datum(this.data);
- this.svg.select("path").attr("d", this.area);
- return this.svg.select(".y.axis").call(this.y_axis);
- };
-
- return ContributorsMasterGraph;
- })(ContributorsGraph);
-
- this.ContributorsAuthorGraph = (function(superClass) {
- extend(ContributorsAuthorGraph, superClass);
-
- function ContributorsAuthorGraph(data1) {
- this.data = data1;
- // Don't split graph size in half for mobile devices.
- if ($(window).width() < 768) {
- this.width = $('.content').width() - 80;
- } else {
- this.width = ($('.content').width() / 2) - 100;
- }
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.list_item = null;
- }
-
- ContributorsAuthorGraph.prototype.create_scale = function() {
- return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
- };
-
- ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
- var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
- return x(parseDate(d));
- }).y0(this.height).y1((function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this)).interpolate("basis");
- };
-
- ContributorsAuthorGraph.prototype.create_svg = function() {
- this.list_item = d3.selectAll(".person")[0].pop();
- return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
- };
-
- ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
- };
-
- ContributorsAuthorGraph.prototype.draw = function() {
- this.create_scale();
- this.create_axes();
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.draw_path(this.dates);
- this.draw_x_axis();
- return this.draw_y_axis();
- };
-
- ContributorsAuthorGraph.prototype.redraw = function() {
- this.set_domain();
- this.svg.select("path").datum(this.dates);
- this.svg.select("path").attr("d", this.area);
- this.svg.select(".x.axis").call(this.x_axis);
- return this.svg.select(".y.axis").call(this.y_axis);
- };
-
- return ContributorsAuthorGraph;
- })(ContributorsGraph);
-}).call(window);
+ return x(parseDate(d));
+ }).y0(this.height).y1((function(_this) {
+ return function(d) {
+ if (_this.data[d] != null) {
+ return y(_this.data[d]);
+ } else {
+ return y(0);
+ }
+ };
+ })(this)).interpolate("basis");
+ };
+
+ ContributorsAuthorGraph.prototype.create_svg = function() {
+ this.list_item = d3.selectAll(".person")[0].pop();
+ return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsAuthorGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+ };
+
+ ContributorsAuthorGraph.prototype.draw = function() {
+ this.create_scale();
+ this.create_axes();
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.draw_path(this.dates);
+ this.draw_x_axis();
+ return this.draw_y_axis();
+ };
+
+ ContributorsAuthorGraph.prototype.redraw = function() {
+ this.set_domain();
+ this.svg.select("path").datum(this.dates);
+ this.svg.select("path").attr("d", this.area);
+ this.svg.select(".x.axis").call(this.x_axis);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsAuthorGraph;
+})(ContributorsGraph);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 7954c583598..c583757f3f2 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,138 +1,137 @@
/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
-(function() {
- window.ContributorsStatGraphUtil = {
- parse_log: function(log) {
- var by_author, by_email, data, entry, i, len, total, normalized_email;
- total = {};
- by_author = {};
- by_email = {};
- for (i = 0, len = log.length; i < len; i += 1) {
- entry = log[i];
- if (total[entry.date] == null) {
- this.add_date(entry.date, total);
- }
- normalized_email = entry.author_email.toLowerCase();
- data = by_author[entry.author_name] || by_email[normalized_email];
- if (data == null) {
- data = this.add_author(entry, by_author, by_email);
- }
- if (!data[entry.date]) {
- this.add_date(entry.date, data);
- }
- this.store_data(entry, total[entry.date], data[entry.date]);
- }
- total = _.toArray(total);
- by_author = _.toArray(by_author);
- return {
- total: total,
- by_author: by_author
- };
- },
- add_date: function(date, collection) {
- collection[date] = {};
- return collection[date].date = date;
- },
- add_author: function(author, by_author, by_email) {
- var data, normalized_email;
- data = {};
- data.author_name = author.author_name;
- data.author_email = author.author_email;
- normalized_email = author.author_email.toLowerCase();
- by_author[author.author_name] = data;
- by_email[normalized_email] = data;
- return data;
- },
- store_data: function(entry, total, by_author) {
- this.store_commits(total, by_author);
- this.store_additions(entry, total, by_author);
- return this.store_deletions(entry, total, by_author);
- },
- store_commits: function(total, by_author) {
- this.add(total, "commits", 1);
- return this.add(by_author, "commits", 1);
- },
- add: function(collection, field, value) {
- if (collection[field] == null) {
- collection[field] = 0;
- }
- return collection[field] += value;
- },
- store_additions: function(entry, total, by_author) {
- if (entry.additions == null) {
- entry.additions = 0;
+
+export default {
+ parse_log: function(log) {
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
+ total = {};
+ by_author = {};
+ by_email = {};
+ for (i = 0, len = log.length; i < len; i += 1) {
+ entry = log[i];
+ if (total[entry.date] == null) {
+ this.add_date(entry.date, total);
}
- this.add(total, "additions", entry.additions);
- return this.add(by_author, "additions", entry.additions);
- },
- store_deletions: function(entry, total, by_author) {
- if (entry.deletions == null) {
- entry.deletions = 0;
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
+ if (data == null) {
+ data = this.add_author(entry, by_author, by_email);
}
- this.add(total, "deletions", entry.deletions);
- return this.add(by_author, "deletions", entry.deletions);
- },
- get_total_data: function(parsed_log, field) {
- var log, total_data;
- log = parsed_log.total;
- total_data = this.pick_field(log, field);
- return _.sortBy(total_data, function(d) {
- return d.date;
- });
- },
- pick_field: function(log, field) {
- var total_data;
- total_data = [];
- _.each(log, function(d) {
- return total_data.push(_.pick(d, [field, 'date']));
- });
- return total_data;
- },
- get_author_data: function(parsed_log, field, date_range) {
- var author_data, log;
- if (date_range == null) {
- date_range = null;
- }
- log = parsed_log.by_author;
- author_data = [];
- _.each(log, (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this));
- return _.sortBy(author_data, function(d) {
- return d[field];
- }).reverse();
- },
- parse_log_entry: function(log_entry, field, date_range) {
- var parsed_entry;
- parsed_entry = {};
- parsed_entry.author_name = log_entry.author_name;
- parsed_entry.author_email = log_entry.author_email;
- parsed_entry.dates = {};
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
- _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
- return function(value, key) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return parsed_entry.deletions += value.deletions;
- }
- };
- })(this));
- return parsed_entry;
- },
- in_range: function(date, date_range) {
- var ref;
- if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
- return true;
- } else {
- return false;
+ if (!data[entry.date]) {
+ this.add_date(entry.date, data);
}
+ this.store_data(entry, total[entry.date], data[entry.date]);
+ }
+ total = _.toArray(total);
+ by_author = _.toArray(by_author);
+ return {
+ total: total,
+ by_author: by_author
+ };
+ },
+ add_date: function(date, collection) {
+ collection[date] = {};
+ return collection[date].date = date;
+ },
+ add_author: function(author, by_author, by_email) {
+ var data, normalized_email;
+ data = {};
+ data.author_name = author.author_name;
+ data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
+ by_author[author.author_name] = data;
+ by_email[normalized_email] = data;
+ return data;
+ },
+ store_data: function(entry, total, by_author) {
+ this.store_commits(total, by_author);
+ this.store_additions(entry, total, by_author);
+ return this.store_deletions(entry, total, by_author);
+ },
+ store_commits: function(total, by_author) {
+ this.add(total, "commits", 1);
+ return this.add(by_author, "commits", 1);
+ },
+ add: function(collection, field, value) {
+ if (collection[field] == null) {
+ collection[field] = 0;
+ }
+ return collection[field] += value;
+ },
+ store_additions: function(entry, total, by_author) {
+ if (entry.additions == null) {
+ entry.additions = 0;
+ }
+ this.add(total, "additions", entry.additions);
+ return this.add(by_author, "additions", entry.additions);
+ },
+ store_deletions: function(entry, total, by_author) {
+ if (entry.deletions == null) {
+ entry.deletions = 0;
+ }
+ this.add(total, "deletions", entry.deletions);
+ return this.add(by_author, "deletions", entry.deletions);
+ },
+ get_total_data: function(parsed_log, field) {
+ var log, total_data;
+ log = parsed_log.total;
+ total_data = this.pick_field(log, field);
+ return _.sortBy(total_data, function(d) {
+ return d.date;
+ });
+ },
+ pick_field: function(log, field) {
+ var total_data;
+ total_data = [];
+ _.each(log, function(d) {
+ return total_data.push(_.pick(d, [field, 'date']));
+ });
+ return total_data;
+ },
+ get_author_data: function(parsed_log, field, date_range) {
+ var author_data, log;
+ if (date_range == null) {
+ date_range = null;
+ }
+ log = parsed_log.by_author;
+ author_data = [];
+ _.each(log, (function(_this) {
+ return function(log_entry) {
+ var parsed_log_entry;
+ parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+ if (!_.isEmpty(parsed_log_entry.dates)) {
+ return author_data.push(parsed_log_entry);
+ }
+ };
+ })(this));
+ return _.sortBy(author_data, function(d) {
+ return d[field];
+ }).reverse();
+ },
+ parse_log_entry: function(log_entry, field, date_range) {
+ var parsed_entry;
+ parsed_entry = {};
+ parsed_entry.author_name = log_entry.author_name;
+ parsed_entry.author_email = log_entry.author_email;
+ parsed_entry.dates = {};
+ parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+ _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
+ return function(value, key) {
+ if (_this.in_range(value.date, date_range)) {
+ parsed_entry.dates[value.date] = value[field];
+ parsed_entry.commits += value.commits;
+ parsed_entry.additions += value.additions;
+ return parsed_entry.deletions += value.deletions;
+ }
+ };
+ })(this));
+ return parsed_entry;
+ },
+ in_range: function(date, date_range) {
+ var ref;
+ if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
+ return true;
+ } else {
+ return false;
}
- };
-}).call(window);
+ }
+};
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index c5cb273c5b2..f03b47b1c1d 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,20 +1,19 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.GroupAvatar = (function() {
- function GroupAvatar() {
- $('.js-choose-group-avatar-button').on("click", function() {
- var form;
- form = $(this).closest("form");
- return form.find(".js-group-avatar-input").click();
- });
- $('.js-group-avatar-input').on("change", function() {
- var filename, form;
- form = $(this).closest("form");
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find(".js-avatar-filename").text(filename);
- });
- }
- return GroupAvatar;
- })();
-}).call(window);
+window.GroupAvatar = (function() {
+ function GroupAvatar() {
+ $('.js-choose-group-avatar-button').on("click", function() {
+ var form;
+ form = $(this).closest("form");
+ return form.find(".js-group-avatar-input").click();
+ });
+ $('.js-group-avatar-input').on("change", function() {
+ var filename, form;
+ form = $(this).closest("form");
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find(".js-avatar-filename").text(filename);
+ });
+ }
+
+ return GroupAvatar;
+})();
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
new file mode 100644
index 00000000000..7dc9ce898e8
--- /dev/null
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -0,0 +1,52 @@
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
+
+class GroupLabelSubscription {
+ constructor(container) {
+ const $container = $(container);
+ this.$dropdown = $container.find('.dropdown');
+ this.$subscribeButtons = $container.find('.js-subscribe-button');
+ this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
+
+ this.$subscribeButtons.on('click', this.subscribe.bind(this));
+ this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
+ }
+
+ unsubscribe(event) {
+ event.preventDefault();
+
+ const url = this.$unsubscribeButtons.attr('data-url');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ this.$unsubscribeButtons.removeAttr('data-url');
+ });
+ }
+
+ subscribe(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const url = $btn.attr('data-url');
+
+ this.$unsubscribeButtons.attr('data-url', url);
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ });
+ }
+
+ toggleSubscriptionButtons() {
+ this.$dropdown.toggleClass('hidden');
+ this.$subscribeButtons.toggleClass('hidden');
+ this.$unsubscribeButtons.toggleClass('hidden');
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.GroupLabelSubscription = GroupLabelSubscription;
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
deleted file mode 100644
index 15e695e81cf..00000000000
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
-
-(function(global) {
- class GroupLabelSubscription {
- constructor(container) {
- const $container = $(container);
- this.$dropdown = $container.find('.dropdown');
- this.$subscribeButtons = $container.find('.js-subscribe-button');
- this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
-
- this.$subscribeButtons.on('click', this.subscribe.bind(this));
- this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
- }
-
- unsubscribe(event) {
- event.preventDefault();
-
- const url = this.$unsubscribeButtons.attr('data-url');
-
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- this.toggleSubscriptionButtons();
- this.$unsubscribeButtons.removeAttr('data-url');
- });
- }
-
- subscribe(event) {
- event.preventDefault();
-
- const $btn = $(event.currentTarget);
- const url = $btn.attr('data-url');
-
- this.$unsubscribeButtons.attr('data-url', url);
-
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- this.toggleSubscriptionButtons();
- });
- }
-
- toggleSubscriptionButtons() {
- this.$dropdown.toggleClass('hidden');
- this.$subscribeButtons.toggleClass('hidden');
- this.$unsubscribeButtons.toggleClass('hidden');
- }
- }
-
- global.GroupLabelSubscription = GroupLabelSubscription;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
new file mode 100644
index 00000000000..6a028f299b1
--- /dev/null
+++ b/app/assets/javascripts/group_name.js
@@ -0,0 +1,40 @@
+const GROUP_LIMIT = 2;
+
+export default class GroupName {
+ constructor() {
+ this.titleContainer = document.querySelector('.title');
+ this.groups = document.querySelectorAll('.group-path');
+ this.groupTitle = document.querySelector('.group-title');
+ this.toggle = null;
+ this.isHidden = false;
+ this.init();
+ }
+
+ init() {
+ if (this.groups.length > GROUP_LIMIT) {
+ this.groups[this.groups.length - 1].classList.remove('hidable');
+ this.addToggle();
+ }
+ this.render();
+ }
+
+ addToggle() {
+ const header = document.querySelector('.header-content');
+ this.toggle = document.createElement('button');
+ this.toggle.className = 'text-expander group-name-toggle';
+ this.toggle.setAttribute('aria-label', 'Toggle full path');
+ this.toggle.innerHTML = '...';
+ this.toggle.addEventListener('click', this.toggleGroups.bind(this));
+ header.insertBefore(this.toggle, this.titleContainer);
+ this.toggleGroups();
+ }
+
+ toggleGroups() {
+ this.isHidden = !this.isHidden;
+ this.groupTitle.classList.toggle('is-hidden');
+ }
+
+ render() {
+ this.titleContainer.classList.remove('initializing');
+ }
+}
diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js
new file mode 100644
index 00000000000..56a8cbf6d03
--- /dev/null
+++ b/app/assets/javascripts/groups_list.js
@@ -0,0 +1,18 @@
+import FilterableList from './filterable_list';
+
+/**
+ * Makes search request for groups when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+export default class GroupsList {
+ constructor() {
+ const form = document.querySelector('form#group-filter-form');
+ const filter = document.querySelector('.js-groups-list-filter');
+ const holder = document.querySelector('.js-groups-list-holder');
+
+ if (form && filter && holder) {
+ const list = new FilterableList(form, filter, holder);
+ list.initSearch();
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 6b937e7fa0f..e5dfa30edab 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,71 +1,69 @@
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
/* global Api */
-(function() {
- var slice = [].slice;
+var slice = [].slice;
- this.GroupsSelect = (function() {
- function GroupsSelect() {
- $('.ajax-groups-select').each((function(_this) {
- return function(i, select) {
- var all_available, skip_groups;
- all_available = $(select).data('all-available');
- skip_groups = $(select).data('skip-groups') || [];
- return $(select).select2({
- placeholder: "Search for a group",
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- var options = { all_available: all_available, skip_groups: skip_groups };
- return Api.groups(query.term, options, function(groups) {
- var data;
- data = {
- results: groups
- };
- return query.callback(data);
- });
- },
- initSelection: function(element, callback) {
- var id;
- id = $(element).val();
- if (id !== "") {
- return Api.group(id, callback);
- }
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: "ajax-groups-dropdown",
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
+window.GroupsSelect = (function() {
+ function GroupsSelect() {
+ $('.ajax-groups-select').each((function(_this) {
+ return function(i, select) {
+ var all_available, skip_groups;
+ all_available = $(select).data('all-available');
+ skip_groups = $(select).data('skip-groups') || [];
+ return $(select).select2({
+ placeholder: "Search for a group",
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ var options = { all_available: all_available, skip_groups: skip_groups };
+ return Api.groups(query.term, options, function(groups) {
+ var data;
+ data = {
+ results: groups
+ };
+ return query.callback(data);
+ });
+ },
+ initSelection: function(element, callback) {
+ var id;
+ id = $(element).val();
+ if (id !== "") {
+ return Api.group(id, callback);
}
- });
- };
- })(this));
- }
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: "ajax-groups-dropdown",
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ }
+ });
+ };
+ })(this));
+ }
- GroupsSelect.prototype.formatResult = function(group) {
- var avatar;
- if (group.avatar_url) {
- avatar = group.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
- }
- return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
- };
+ GroupsSelect.prototype.formatResult = function(group) {
+ var avatar;
+ if (group.avatar_url) {
+ avatar = group.avatar_url;
+ } else {
+ avatar = gon.default_avatar_url;
+ }
+ return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
+ };
- GroupsSelect.prototype.formatSelection = function(group) {
- return group.full_name;
- };
+ GroupsSelect.prototype.formatSelection = function(group) {
+ return group.full_name;
+ };
- return GroupsSelect;
- })();
-}).call(window);
+ return GroupsSelect;
+})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index a853c3aeb1f..34f44dad7a5 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,8 +1,7 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
-(function() {
- $(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-pending-count');
- $todoPendingCount.text(gl.text.highCountTrim(count));
- $todoPendingCount.toggleClass('hidden', count === 0);
- });
-})();
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
+
+$(document).on('todo:toggle', function(e, count) {
+ var $todoPendingCount = $('.todos-pending-count');
+ $todoPendingCount.text(gl.text.highCountTrim(count));
+ $todoPendingCount.toggleClass('hidden', count === 0);
+});
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js
new file mode 100644
index 00000000000..3bfce32768a
--- /dev/null
+++ b/app/assets/javascripts/issuable.js
@@ -0,0 +1,188 @@
+/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
+/* global Issuable */
+
+((global) => {
+ var issuable_created;
+
+ issuable_created = false;
+
+ global.Issuable = {
+ init: function() {
+ Issuable.initTemplates();
+ Issuable.initSearch();
+ Issuable.initChecks();
+ Issuable.initResetFilters();
+ Issuable.resetIncomingEmailToken();
+ return Issuable.initLabelFilterRemove();
+ },
+ initTemplates: function() {
+ return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
+ },
+ initSearch: function() {
+ const $searchInput = $('#issuable_search');
+
+ Issuable.initSearchState($searchInput);
+
+ // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
+
+ $searchInput.off('keyup').on('keyup', debouncedExecSearch);
+
+ // ensures existing filters are preserved when manually submitted
+ $('#issuable_search_form').on('submit', (e) => {
+ e.preventDefault();
+ debouncedExecSearch(e);
+ });
+ },
+ initSearchState: function($searchInput) {
+ const currentSearchVal = $searchInput.val();
+
+ Issuable.searchState = {
+ elem: $searchInput,
+ current: currentSearchVal
+ };
+
+ Issuable.maybeFocusOnSearch();
+ },
+ accessSearchPristine: function(set) {
+ // store reference to previous value to prevent search on non-mutating keyup
+ const state = Issuable.searchState;
+ const currentSearchVal = state.elem.val();
+
+ if (set) {
+ state.current = currentSearchVal;
+ } else {
+ return state.current === currentSearchVal;
+ }
+ },
+ maybeFocusOnSearch: function() {
+ const currentSearchVal = Issuable.searchState.current;
+ if (currentSearchVal && currentSearchVal !== '') {
+ const queryLength = currentSearchVal.length;
+ const $searchInput = Issuable.searchState.elem;
+
+ /* The following ensures that the cursor is initially placed at
+ * the end of search input when focus is applied. It accounts
+ * for differences in browser implementations of `setSelectionRange`
+ * and cursor placement for elements in focus.
+ */
+ $searchInput.focus();
+ if ($searchInput.setSelectionRange) {
+ $searchInput.setSelectionRange(queryLength, queryLength);
+ } else {
+ $searchInput.val(currentSearchVal);
+ }
+ }
+ },
+ executeSearch: function(e) {
+ const $search = $('#issuable_search');
+ const $searchName = $search.attr('name');
+ const $searchValue = $search.val();
+ const $filtersForm = $('.js-filter-form');
+ const $input = $(`input[name='${$searchName}']`, $filtersForm);
+ const isPristine = Issuable.accessSearchPristine();
+
+ if (isPristine) {
+ return;
+ }
+
+ if (!$input.length) {
+ $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
+ } else {
+ $input.val($searchValue);
+ }
+
+ Issuable.filterResults($filtersForm);
+ },
+ initLabelFilterRemove: function() {
+ return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
+ var $button;
+ $button = $(this);
+ // Remove the label input box
+ $('input[name="label_name[]"]').filter(function() {
+ return this.value === $button.data('label');
+ }).remove();
+ // Submit the form to get new data
+ Issuable.filterResults($('.filter-form'));
+ });
+ },
+ filterResults: (function(_this) {
+ return function(form) {
+ var formAction, formData, issuesUrl;
+ formData = form.serializeArray();
+ formData = formData.filter(function(data) {
+ return data.value !== '';
+ });
+ formData = $.param(formData);
+ formAction = form.attr('action');
+ issuesUrl = formAction;
+ issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
+ issuesUrl += formData;
+ return gl.utils.visitUrl(issuesUrl);
+ };
+ })(this),
+ initResetFilters: function() {
+ $('.reset-filters').on('click', function(e) {
+ e.preventDefault();
+ const target = e.target;
+ const $form = $(target).parents('.js-filter-form');
+ const baseIssuesUrl = target.href;
+
+ $form.attr('action', baseIssuesUrl);
+ gl.utils.visitUrl(baseIssuesUrl);
+ });
+ },
+ initChecks: function() {
+ this.issuableBulkActions = $('.bulk-update').data('bulkActions');
+ $('.check_all_issues').off('click').on('click', function() {
+ $('.selected_issue').prop('checked', this.checked);
+ return Issuable.checkChanged();
+ });
+ return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
+ },
+ checkChanged: function() {
+ const $checkedIssues = $('.selected_issue:checked');
+ const $updateIssuesIds = $('#update_issuable_ids');
+ const $issuesOtherFilters = $('.issues-other-filters');
+ const $issuesBulkUpdate = $('.issues_bulk_update');
+
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
+ if ($checkedIssues.length > 0) {
+ const ids = $.map($checkedIssues, function(value) {
+ return $(value).data('id');
+ });
+ $updateIssuesIds.val(ids);
+ $issuesOtherFilters.hide();
+ $issuesBulkUpdate.show();
+ } else {
+ $updateIssuesIds.val([]);
+ $issuesBulkUpdate.hide();
+ $issuesOtherFilters.show();
+ }
+ return true;
+ },
+
+ resetIncomingEmailToken: function() {
+ $('.incoming-email-token-reset').on('click', function(e) {
+ e.preventDefault();
+
+ $.ajax({
+ type: 'PUT',
+ url: $('.incoming-email-token-reset').attr('href'),
+ dataType: 'json',
+ success: function(response) {
+ $('#issue_email').val(response.new_issue_address).focus();
+ },
+ beforeSend: function() {
+ $('.incoming-email-token-reset').text('resetting...');
+ },
+ complete: function() {
+ $('.incoming-email-token-reset').text('reset it');
+ }
+ });
+ });
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
deleted file mode 100644
index 8df86f68218..00000000000
--- a/app/assets/javascripts/issuable.js.es6
+++ /dev/null
@@ -1,188 +0,0 @@
-/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
-/* global Issuable */
-
-((global) => {
- var issuable_created;
-
- issuable_created = false;
-
- global.Issuable = {
- init: function() {
- Issuable.initTemplates();
- Issuable.initSearch();
- Issuable.initChecks();
- Issuable.initResetFilters();
- Issuable.resetIncomingEmailToken();
- return Issuable.initLabelFilterRemove();
- },
- initTemplates: function() {
- return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
- },
- initSearch: function() {
- const $searchInput = $('#issuable_search');
-
- Issuable.initSearchState($searchInput);
-
- // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
- const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
-
- $searchInput.off('keyup').on('keyup', debouncedExecSearch);
-
- // ensures existing filters are preserved when manually submitted
- $('#issuable_search_form').on('submit', (e) => {
- e.preventDefault();
- debouncedExecSearch(e);
- });
- },
- initSearchState: function($searchInput) {
- const currentSearchVal = $searchInput.val();
-
- Issuable.searchState = {
- elem: $searchInput,
- current: currentSearchVal
- };
-
- Issuable.maybeFocusOnSearch();
- },
- accessSearchPristine: function(set) {
- // store reference to previous value to prevent search on non-mutating keyup
- const state = Issuable.searchState;
- const currentSearchVal = state.elem.val();
-
- if (set) {
- state.current = currentSearchVal;
- } else {
- return state.current === currentSearchVal;
- }
- },
- maybeFocusOnSearch: function() {
- const currentSearchVal = Issuable.searchState.current;
- if (currentSearchVal && currentSearchVal !== '') {
- const queryLength = currentSearchVal.length;
- const $searchInput = Issuable.searchState.elem;
-
- /* The following ensures that the cursor is initially placed at
- * the end of search input when focus is applied. It accounts
- * for differences in browser implementations of `setSelectionRange`
- * and cursor placement for elements in focus.
- */
- $searchInput.focus();
- if ($searchInput.setSelectionRange) {
- $searchInput.setSelectionRange(queryLength, queryLength);
- } else {
- $searchInput.val(currentSearchVal);
- }
- }
- },
- executeSearch: function(e) {
- const $search = $('#issuable_search');
- const $searchName = $search.attr('name');
- const $searchValue = $search.val();
- const $filtersForm = $('.js-filter-form');
- const $input = $(`input[name='${$searchName}']`, $filtersForm);
- const isPristine = Issuable.accessSearchPristine();
-
- if (isPristine) {
- return;
- }
-
- if (!$input.length) {
- $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
- } else {
- $input.val($searchValue);
- }
-
- Issuable.filterResults($filtersForm);
- },
- initLabelFilterRemove: function() {
- return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
- var $button;
- $button = $(this);
- // Remove the label input box
- $('input[name="label_name[]"]').filter(function() {
- return this.value === $button.data('label');
- }).remove();
- // Submit the form to get new data
- Issuable.filterResults($('.filter-form'));
- });
- },
- filterResults: (function(_this) {
- return function(form) {
- var formAction, formData, issuesUrl;
- formData = form.serializeArray();
- formData = formData.filter(function(data) {
- return data.value !== '';
- });
- formData = $.param(formData);
- formAction = form.attr('action');
- issuesUrl = formAction;
- issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
- issuesUrl += formData;
- return gl.utils.visitUrl(issuesUrl);
- };
- })(this),
- initResetFilters: function() {
- $('.reset-filters').on('click', function(e) {
- e.preventDefault();
- const target = e.target;
- const $form = $(target).parents('.js-filter-form');
- const baseIssuesUrl = target.href;
-
- $form.attr('action', baseIssuesUrl);
- gl.utils.visitUrl(baseIssuesUrl);
- });
- },
- initChecks: function() {
- this.issuableBulkActions = $('.bulk-update').data('bulkActions');
- $('.check_all_issues').off('click').on('click', function() {
- $('.selected_issue').prop('checked', this.checked);
- return Issuable.checkChanged();
- });
- return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
- },
- checkChanged: function() {
- const $checkedIssues = $('.selected_issue:checked');
- const $updateIssuesIds = $('#update_issuable_ids');
- const $issuesOtherFilters = $('.issues-other-filters');
- const $issuesBulkUpdate = $('.issues_bulk_update');
-
- this.issuableBulkActions.willUpdateLabels = false;
- this.issuableBulkActions.setOriginalDropdownData();
-
- if ($checkedIssues.length > 0) {
- const ids = $.map($checkedIssues, function(value) {
- return $(value).data('id');
- });
- $updateIssuesIds.val(ids);
- $issuesOtherFilters.hide();
- $issuesBulkUpdate.show();
- } else {
- $updateIssuesIds.val([]);
- $issuesBulkUpdate.hide();
- $issuesOtherFilters.show();
- }
- return true;
- },
-
- resetIncomingEmailToken: function() {
- $('.incoming-email-token-reset').on('click', function(e) {
- e.preventDefault();
-
- $.ajax({
- type: 'PUT',
- url: $('.incoming-email-token-reset').attr('href'),
- dataType: 'json',
- success: function(response) {
- $('#issue_email').val(response.new_issue_address).focus();
- },
- beforeSend: function() {
- $('.incoming-email-token-reset').text('resetting...');
- },
- complete: function() {
- $('.incoming-email-token-reset').text('reset it');
- }
- });
- });
- }
- };
-})(window);
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js
index e927cc0077c..e927cc0077c 100644
--- a/app/assets/javascripts/issuable/issuable_bundle.js.es6
+++ b/app/assets/javascripts/issuable/issuable_bundle.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
new file mode 100644
index 00000000000..357b3487ca9
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
@@ -0,0 +1,42 @@
+/* global Vue */
+import stopwatchSvg from 'icons/_icon_stopwatch.svg';
+
+require('../../../lib/utils/pretty_time');
+
+(() => {
+ Vue.component('time-tracking-collapsed-state', {
+ name: 'time-tracking-collapsed-state',
+ props: [
+ 'showComparisonState',
+ 'showSpentOnlyState',
+ 'showEstimateOnlyState',
+ 'showNoTimeTrackingState',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ ],
+ methods: {
+ abbreviateTime(timeStr) {
+ return gl.utils.prettyTime.abbreviateTime(timeStr);
+ },
+ },
+ template: `
+ <div class='sidebar-collapsed-icon'>
+ ${stopwatchSvg}
+ <div class='time-tracking-collapsed-summary'>
+ <div class='compare' v-if='showComparisonState'>
+ <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='estimate-only' v-if='showEstimateOnlyState'>
+ <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='spend-only' v-if='showSpentOnlyState'>
+ <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
+ </div>
+ <div class='no-tracking' v-if='showNoTimeTrackingState'>
+ <span class='no-value'>None</span>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
deleted file mode 100644
index bf27fbac5d7..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
+++ /dev/null
@@ -1,41 +0,0 @@
-/* global Vue */
-require('../../../lib/utils/pretty_time');
-
-(() => {
- Vue.component('time-tracking-collapsed-state', {
- name: 'time-tracking-collapsed-state',
- props: [
- 'showComparisonState',
- 'showSpentOnlyState',
- 'showEstimateOnlyState',
- 'showNoTimeTrackingState',
- 'timeSpentHumanReadable',
- 'timeEstimateHumanReadable',
- 'stopwatchSvg',
- ],
- methods: {
- abbreviateTime(timeStr) {
- return gl.utils.prettyTime.abbreviateTime(timeStr);
- },
- },
- template: `
- <div class='sidebar-collapsed-icon'>
- <div v-html='stopwatchSvg'></div>
- <div class='time-tracking-collapsed-summary'>
- <div class='compare' v-if='showComparisonState'>
- <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
- </div>
- <div class='estimate-only' v-if='showEstimateOnlyState'>
- <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
- </div>
- <div class='spend-only' v-if='showSpentOnlyState'>
- <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
- </div>
- <div class='no-tracking' v-if='showNoTimeTrackingState'>
- <span class='no-value'>None</span>
- </div>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
index 750468c679b..750468c679b 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
index 309e9f2f9ef..309e9f2f9ef 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
index d7ced6d7151..d7ced6d7151 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
index 1d2ca643b5b..1d2ca643b5b 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
index ed283fec3c3..ed283fec3c3 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
new file mode 100644
index 00000000000..1fae2d62b14
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
@@ -0,0 +1,117 @@
+/* global Vue */
+
+require('./help_state');
+require('./collapsed_state');
+require('./spent_only_pane');
+require('./no_tracking_pane');
+require('./estimate_only_pane');
+require('./comparison_pane');
+
+(() => {
+ Vue.component('issuable-time-tracker', {
+ name: 'issuable-time-tracker',
+ props: [
+ 'time_estimate',
+ 'time_spent',
+ 'human_time_estimate',
+ 'human_time_spent',
+ 'docsUrl',
+ ],
+ data() {
+ return {
+ showHelp: false,
+ };
+ },
+ computed: {
+ timeSpent() {
+ return this.time_spent;
+ },
+ timeEstimate() {
+ return this.time_estimate;
+ },
+ timeEstimateHumanReadable() {
+ return this.human_time_estimate;
+ },
+ timeSpentHumanReadable() {
+ return this.human_time_spent;
+ },
+ hasTimeSpent() {
+ return !!this.timeSpent;
+ },
+ hasTimeEstimate() {
+ return !!this.timeEstimate;
+ },
+ showComparisonState() {
+ return this.hasTimeEstimate && this.hasTimeSpent;
+ },
+ showEstimateOnlyState() {
+ return this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showSpentOnlyState() {
+ return this.hasTimeSpent && !this.hasTimeEstimate;
+ },
+ showNoTimeTrackingState() {
+ return !this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showHelpState() {
+ return !!this.showHelp;
+ },
+ },
+ methods: {
+ toggleHelpState(show) {
+ this.showHelp = show;
+ },
+ },
+ template: `
+ <div class='time_tracker time-tracking-component-wrap' v-cloak>
+ <time-tracking-collapsed-state
+ :show-comparison-state='showComparisonState'
+ :show-help-state='showHelpState'
+ :show-spent-only-state='showSpentOnlyState'
+ :show-estimate-only-state='showEstimateOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-collapsed-state>
+ <div class='title hide-collapsed'>
+ Time tracking
+ <div class='help-button pull-right'
+ v-if='!showHelpState'
+ @click='toggleHelpState(true)'>
+ <i class='fa fa-question-circle' aria-hidden='true'></i>
+ </div>
+ <div class='close-help-button pull-right'
+ v-if='showHelpState'
+ @click='toggleHelpState(false)'>
+ <i class='fa fa-close' aria-hidden='true'></i>
+ </div>
+ </div>
+ <div class='time-tracking-content hide-collapsed'>
+ <time-tracking-estimate-only-pane
+ v-if='showEstimateOnlyState'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-estimate-only-pane>
+ <time-tracking-spent-only-pane
+ v-if='showSpentOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'>
+ </time-tracking-spent-only-pane>
+ <time-tracking-no-tracking-pane
+ v-if='showNoTimeTrackingState'>
+ </time-tracking-no-tracking-pane>
+ <time-tracking-comparison-pane
+ v-if='showComparisonState'
+ :time-estimate='timeEstimate'
+ :time-spent='timeSpent'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-comparison-pane>
+ <transition name='help-state-toggle'>
+ <time-tracking-help-state
+ v-if='showHelpState'
+ :docs-url='docsUrl'>
+ </time-tracking-help-state>
+ </transition>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
deleted file mode 100644
index e38f7852b1c..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue */
-
-require('./help_state');
-require('./collapsed_state');
-require('./spent_only_pane');
-require('./no_tracking_pane');
-require('./estimate_only_pane');
-require('./comparison_pane');
-
-(() => {
- Vue.component('issuable-time-tracker', {
- name: 'issuable-time-tracker',
- props: [
- 'time_estimate',
- 'time_spent',
- 'human_time_estimate',
- 'human_time_spent',
- 'stopwatchSvg',
- 'docsUrl',
- ],
- data() {
- return {
- showHelp: false,
- };
- },
- computed: {
- timeSpent() {
- return this.time_spent;
- },
- timeEstimate() {
- return this.time_estimate;
- },
- timeEstimateHumanReadable() {
- return this.human_time_estimate;
- },
- timeSpentHumanReadable() {
- return this.human_time_spent;
- },
- hasTimeSpent() {
- return !!this.timeSpent;
- },
- hasTimeEstimate() {
- return !!this.timeEstimate;
- },
- showComparisonState() {
- return this.hasTimeEstimate && this.hasTimeSpent;
- },
- showEstimateOnlyState() {
- return this.hasTimeEstimate && !this.hasTimeSpent;
- },
- showSpentOnlyState() {
- return this.hasTimeSpent && !this.hasTimeEstimate;
- },
- showNoTimeTrackingState() {
- return !this.hasTimeEstimate && !this.hasTimeSpent;
- },
- showHelpState() {
- return !!this.showHelp;
- },
- },
- methods: {
- toggleHelpState(show) {
- this.showHelp = show;
- },
- },
- template: `
- <div class='time_tracker time-tracking-component-wrap' v-cloak>
- <time-tracking-collapsed-state
- :show-comparison-state='showComparisonState'
- :show-help-state='showHelpState'
- :show-spent-only-state='showSpentOnlyState'
- :show-estimate-only-state='showEstimateOnlyState'
- :time-spent-human-readable='timeSpentHumanReadable'
- :time-estimate-human-readable='timeEstimateHumanReadable'
- :stopwatch-svg='stopwatchSvg'>
- </time-tracking-collapsed-state>
- <div class='title hide-collapsed'>
- Time tracking
- <div class='help-button pull-right'
- v-if='!showHelpState'
- @click='toggleHelpState(true)'>
- <i class='fa fa-question-circle'></i>
- </div>
- <div class='close-help-button pull-right'
- v-if='showHelpState'
- @click='toggleHelpState(false)'>
- <i class='fa fa-close'></i>
- </div>
- </div>
- <div class='time-tracking-content hide-collapsed'>
- <time-tracking-estimate-only-pane
- v-if='showEstimateOnlyState'
- :time-estimate-human-readable='timeEstimateHumanReadable'>
- </time-tracking-estimate-only-pane>
- <time-tracking-spent-only-pane
- v-if='showSpentOnlyState'
- :time-spent-human-readable='timeSpentHumanReadable'>
- </time-tracking-spent-only-pane>
- <time-tracking-no-tracking-pane
- v-if='showNoTimeTrackingState'>
- </time-tracking-no-tracking-pane>
- <time-tracking-comparison-pane
- v-if='showComparisonState'
- :time-estimate='timeEstimate'
- :time-spent='timeSpent'
- :time-spent-human-readable='timeSpentHumanReadable'
- :time-estimate-human-readable='timeEstimateHumanReadable'>
- </time-tracking-comparison-pane>
- <transition name='help-state-toggle'>
- <time-tracking-help-state
- v-if='showHelpState'
- :docs-url='docsUrl'>
- </time-tracking-help-state>
- </transition>
- </div>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
new file mode 100644
index 00000000000..0134b7cb6f3
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
@@ -0,0 +1,65 @@
+/* global Vue */
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('./components/time_tracker');
+require('../../smart_interval');
+require('../../subbable_resource');
+
+(() => {
+ /* This Vue instance represents what will become the parent instance for the
+ * sidebar. It will be responsible for managing `issuable` state and propagating
+ * changes to sidebar components. We will want to create a separate service to
+ * interface with the server at that point.
+ */
+
+ class IssuableTimeTracking {
+ constructor(issuableJSON) {
+ const parsedIssuable = JSON.parse(issuableJSON);
+ return this.initComponent(parsedIssuable);
+ }
+
+ initComponent(parsedIssuable) {
+ this.parentInstance = new Vue({
+ el: '#issuable-time-tracker',
+ data: {
+ issuable: parsedIssuable,
+ },
+ methods: {
+ fetchIssuable() {
+ return gl.IssuableResource.get.call(gl.IssuableResource, {
+ type: 'GET',
+ url: gl.IssuableResource.endpoint,
+ });
+ },
+ updateState(data) {
+ this.issuable = data;
+ },
+ subscribeToUpdates() {
+ gl.IssuableResource.subscribe(data => this.updateState(data));
+ },
+ listenForSlashCommands() {
+ $(document).on('ajax:success', '.gfm-form', (e, data) => {
+ const subscribedCommands = ['spend_time', 'time_estimate'];
+ const changedCommands = data.commands_changes
+ ? Object.keys(data.commands_changes)
+ : [];
+ if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
+ this.fetchIssuable();
+ }
+ });
+ },
+ },
+ created() {
+ this.fetchIssuable();
+ },
+ mounted() {
+ this.subscribeToUpdates();
+ this.listenForSlashCommands();
+ },
+ });
+ }
+ }
+
+ gl.IssuableTimeTracking = IssuableTimeTracking;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
deleted file mode 100644
index 1ca01d3bdb9..00000000000
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
+++ /dev/null
@@ -1,62 +0,0 @@
-/* global Vue */
-
-require('./components/time_tracker');
-require('../../smart_interval');
-require('../../subbable_resource');
-
-(() => {
- /* This Vue instance represents what will become the parent instance for the
- * sidebar. It will be responsible for managing `issuable` state and propagating
- * changes to sidebar components. We will want to create a separate service to
- * interface with the server at that point.
- */
-
- class IssuableTimeTracking {
- constructor(issuableJSON) {
- const parsedIssuable = JSON.parse(issuableJSON);
- return this.initComponent(parsedIssuable);
- }
-
- initComponent(parsedIssuable) {
- this.parentInstance = new Vue({
- el: '#issuable-time-tracker',
- data: {
- issuable: parsedIssuable,
- },
- methods: {
- fetchIssuable() {
- return gl.IssuableResource.get.call(gl.IssuableResource, {
- type: 'GET',
- url: gl.IssuableResource.endpoint,
- });
- },
- updateState(data) {
- this.issuable = data;
- },
- subscribeToUpdates() {
- gl.IssuableResource.subscribe(data => this.updateState(data));
- },
- listenForSlashCommands() {
- $(document).on('ajax:success', '.gfm-form', (e, data) => {
- const subscribedCommands = ['spend_time', 'time_estimate'];
- const changedCommands = data.commands_changes;
-
- if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
- this.fetchIssuable();
- }
- });
- },
- },
- created() {
- this.fetchIssuable();
- },
- mounted() {
- this.subscribeToUpdates();
- this.listenForSlashCommands();
- },
- });
- }
- }
-
- gl.IssuableTimeTracking = IssuableTimeTracking;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 52457f70d90..47e675f537e 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,134 +2,130 @@
/* global Flash */
require('./flash');
+require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
require('./task_list');
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
- this.Issue = (function() {
- function Issue() {
- this.submitNoteForm = bind(this.submitNoteForm, this);
- if ($('a.btn-close').length) {
- this.taskList = new gl.TaskList({
- dataType: 'issue',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- this.initIssueBtnEventListeners();
- }
- this.initMergeRequests();
- this.initRelatedBranches();
- this.initCanCreateBranch();
+class Issue {
+ constructor() {
+ if ($('a.btn-close').length) {
+ this.taskList = new gl.TaskList({
+ dataType: 'issue',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
+ }
+ });
+ Issue.initIssueBtnEventListeners();
}
+ Issue.initMergeRequests();
+ Issue.initRelatedBranches();
+ Issue.initCanCreateBranch();
+ }
- Issue.prototype.initIssueBtnEventListeners = function() {
- var _this, issueFailMessage;
- _this = this;
- issueFailMessage = 'Unable to update this issue at this time.';
- return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, isClose, shouldSubmit, url;
- e.preventDefault();
- e.stopImmediatePropagation();
- $this = $(this);
- isClose = $this.hasClass('btn-close');
- shouldSubmit = $this.hasClass('btn-comment');
- if (shouldSubmit) {
- _this.submitNoteForm($this.closest('form'));
- }
- $this.prop('disabled', true);
- url = $this.attr('href');
- return $.ajax({
- type: 'PUT',
- url: url,
- error: function(jqXHR, textStatus, errorThrown) {
- var issueStatus;
- issueStatus = isClose ? 'close' : 'open';
- return new Flash(issueFailMessage, 'alert');
- },
- success: function(data, textStatus, jqXHR) {
- if ('id' in data) {
- $(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
- if (isClose) {
- $('a.btn-close').addClass('hidden');
- $('a.btn-reopen').removeClass('hidden');
- $('div.status-box-closed').removeClass('hidden');
- $('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
- } else {
- $('a.btn-reopen').addClass('hidden');
- $('a.btn-close').removeClass('hidden');
- $('div.status-box-closed').addClass('hidden');
- $('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
- }
+ static initIssueBtnEventListeners() {
+ var issueFailMessage;
+ issueFailMessage = 'Unable to update this issue at this time.';
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, isClose, shouldSubmit, url;
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $this = $(this);
+ isClose = $this.hasClass('btn-close');
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit) {
+ Issue.submitNoteForm($this.closest('form'));
+ }
+ $this.prop('disabled', true);
+ url = $this.attr('href');
+ return $.ajax({
+ type: 'PUT',
+ url: url,
+ error: function(jqXHR, textStatus, errorThrown) {
+ var issueStatus;
+ issueStatus = isClose ? 'close' : 'open';
+ return new Flash(issueFailMessage, 'alert');
+ },
+ success: function(data, textStatus, jqXHR) {
+ if ('id' in data) {
+ $(document).trigger('issuable:change');
+ let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
+ if (isClose) {
+ $('a.btn-close').addClass('hidden');
+ $('a.btn-reopen').removeClass('hidden');
+ $('div.status-box-closed').removeClass('hidden');
+ $('div.status-box-open').addClass('hidden');
+ total -= 1;
} else {
- new Flash(issueFailMessage, 'alert');
+ $('a.btn-reopen').addClass('hidden');
+ $('a.btn-close').removeClass('hidden');
+ $('div.status-box-closed').addClass('hidden');
+ $('div.status-box-open').removeClass('hidden');
+ total += 1;
}
- return $this.prop('disabled', false);
+ $('.issue_counter').text(gl.text.addDelimiter(total));
+ } else {
+ new Flash(issueFailMessage, 'alert');
}
- });
+ return $this.prop('disabled', false);
+ }
});
- };
+ });
+ }
- Issue.prototype.submitNoteForm = function(form) {
- var noteText;
- noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
- return form.submit();
- }
- };
+ static submitNoteForm(form) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ return form.submit();
+ }
+ }
- Issue.prototype.initMergeRequests = function() {
- var $container;
- $container = $('#merge-requests');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load referenced merge requests', 'alert');
- }).success(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
- };
+ static initMergeRequests() {
+ var $container;
+ $container = $('#merge-requests');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load referenced merge requests', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ }
- Issue.prototype.initRelatedBranches = function() {
- var $container;
- $container = $('#related-branches');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load related branches', 'alert');
- }).success(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
- };
+ static initRelatedBranches() {
+ var $container;
+ $container = $('#related-branches');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load related branches', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ }
- Issue.prototype.initCanCreateBranch = function() {
- var $container;
- $container = $('#new-branch');
- // If the user doesn't have the required permissions the container isn't
- // rendered at all.
- if ($container.length === 0) {
- return;
+ static initCanCreateBranch() {
+ var $container;
+ $container = $('#new-branch');
+ // If the user doesn't have the required permissions the container isn't
+ // rendered at all.
+ if ($container.length === 0) {
+ return;
+ }
+ return $.getJSON($container.data('path')).error(function() {
+ $container.find('.unavailable').show();
+ return new Flash('Failed to check if a new branch can be created.', 'alert');
+ }).success(function(data) {
+ if (data.can_create_branch) {
+ $container.find('.available').show();
+ } else {
+ return $container.find('.unavailable').show();
}
- return $.getJSON($container.data('path')).error(function() {
- $container.find('.unavailable').show();
- return new Flash('Failed to check if a new branch can be created.', 'alert');
- }).success(function(data) {
- if (data.can_create_branch) {
- $container.find('.available').show();
- } else {
- return $container.find('.unavailable').show();
- }
- });
- };
+ });
+ }
+}
- return Issue;
- })();
-}).call(window);
+export default Issue;
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js
index e0ebd36a65c..e0ebd36a65c 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js
index 38b2eb9ff14..38b2eb9ff14 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9e2d14c7f87..443fb3e0ca9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
if (!selected.length) {
data[abilityName].label_ids = [''];
}
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
@@ -353,31 +353,17 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
- !$dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.BoardsStore.state.filters;
- } else if ($dropdown.closest('.add-issues-modal').length) {
+ if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = gl.issueBoards.ModalStore.store.filter;
}
if (boardsModel) {
if (label.isAny) {
boardsModel['label_name'] = [];
- }
- else if ($el.hasClass('is-active')) {
+ } else if ($el.hasClass('is-active')) {
boardsModel['label_name'].push(label.title);
}
- else {
- var filters = boardsModel['label_name'];
- filters = filters.filter(function (filteredLabel) {
- return filteredLabel !== label.title;
- });
- boardsModel['label_name'] = filters;
- }
- if (!$dropdown.closest('.add-issues-modal').length) {
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- }
e.preventDefault();
return;
}
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
deleted file mode 100644
index 9b011d89e93..00000000000
--- a/app/assets/javascripts/lib/chart.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-window.Chart = require('vendor/Chart');
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
deleted file mode 100644
index 7862c6797c3..00000000000
--- a/app/assets/javascripts/lib/cropper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require cropper */
-
-(function() {
-
-}).call(window);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
deleted file mode 100644
index a9dd32edbed..00000000000
--- a/app/assets/javascripts/lib/d3.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-window.d3 = require('d3');
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
deleted file mode 100644
index ebe1e2ae98d..00000000000
--- a/app/assets/javascripts/lib/raphael.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require raphael */
-/*= require g.raphael */
-/*= require g.bar */
-
-(function() {
-
-}).call(window);
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
index 2955bda1a36..2955bda1a36 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
new file mode 100644
index 00000000000..a1423b6fda5
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -0,0 +1,342 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
+(function() {
+ (function(w) {
+ var base;
+ w.gl || (w.gl = {});
+ (base = w.gl).utils || (base.utils = {});
+ w.gl.utils.isInGroupsPage = function() {
+ return gl.utils.getPagePath() === 'groups';
+ };
+ w.gl.utils.isInProjectPage = function() {
+ return gl.utils.getPagePath() === 'projects';
+ };
+ w.gl.utils.getProjectSlug = function() {
+ if (this.isInProjectPage()) {
+ return $('body').data('project');
+ } else {
+ return null;
+ }
+ };
+ w.gl.utils.getGroupSlug = function() {
+ if (this.isInGroupsPage()) {
+ return $('body').data('group');
+ } else {
+ return null;
+ }
+ };
+
+ w.gl.utils.ajaxGet = function(url) {
+ return $.ajax({
+ type: "GET",
+ url: url,
+ dataType: "script"
+ });
+ };
+
+ w.gl.utils.extractLast = function(term) {
+ return this.split(term).pop();
+ };
+
+ w.gl.utils.rstrip = function rstrip(val) {
+ if (val) {
+ return val.replace(/\s+$/, '');
+ } else {
+ return val;
+ }
+ };
+
+ w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
+ event_name = event_name || 'input';
+ var closest_submit, field, that;
+ that = this;
+ field = $(field_selector);
+ closest_submit = field.closest('form').find(button_selector);
+ if (this.rstrip(field.val()) === "") {
+ closest_submit.disable();
+ }
+ return field.on(event_name, function() {
+ if (that.rstrip($(this).val()) === "") {
+ return closest_submit.disable();
+ } else {
+ return closest_submit.enable();
+ }
+ });
+ };
+
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ w.gl.utils.handleLocationHash = function() {
+ var hash = w.gl.utils.getLocationHash();
+ if (!hash) return;
+
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
+
+ // scroll to user-generated markdown anchor if we cannot find a match
+ if (document.getElementById(hash) === null) {
+ var target = document.getElementById('user-content-' + hash);
+ if (target && target.scrollIntoView) {
+ target.scrollIntoView(true);
+ }
+ } else {
+ // only adjust for fixedTabs when not targeting user-generated content
+ var fixedTabs = document.querySelector('.js-tabs-affix');
+ if (fixedTabs) {
+ window.scrollBy(0, -fixedTabs.offsetHeight);
+ }
+ }
+ };
+
+ // Check if element scrolled into viewport from above or below
+ // Courtesy http://stackoverflow.com/a/7557433/414749
+ w.gl.utils.isInViewport = function(el) {
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+ };
+
+ gl.utils.getPagePath = function(index) {
+ index = index || 0;
+ return $('body').data('page').split(':')[index];
+ };
+
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
+
+ gl.utils.parseUrlPathname = function (url) {
+ var parsedUrl = gl.utils.parseUrl(url);
+ // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
+ // We have to make sure we always have an absolute path.
+ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
+ };
+
+ gl.utils.getUrlParamsArray = function () {
+ // We can trust that each param has one & since values containing & will be encoded
+ // Remove the first character of search as it is always ?
+ return window.location.search.slice(1).split('&');
+ };
+
+ gl.utils.isMetaKey = function(e) {
+ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
+ };
+
+ gl.utils.isMetaClick = function(e) {
+ // Identify following special clicks
+ // 1) Cmd + Click on Mac (e.metaKey)
+ // 2) Ctrl + Click on PC (e.ctrlKey)
+ // 3) Middle-click or Mouse Wheel Click (e.which is 2)
+ return e.metaKey || e.ctrlKey || e.which === 2;
+ };
+
+ gl.utils.scrollToElement = function($el) {
+ var top = $el.offset().top;
+ gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
+
+ return $('body, html').animate({
+ scrollTop: top - (gl.mrTabsHeight)
+ }, 200);
+ };
+
+ /**
+ this will take in the `name` of the param you want to parse in the url
+ if the name does not exist this function will return `null`
+ otherwise it will return the value of the param key provided
+ */
+ w.gl.utils.getParameterByName = (name) => {
+ const url = window.location.href;
+ name = name.replace(/[[\]]/g, '\\$&');
+ const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
+ const results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ };
+
+ w.gl.utils.getSelectedFragment = () => {
+ const selection = window.getSelection();
+ if (selection.rangeCount === 0) return null;
+ const documentFragment = selection.getRangeAt(0).cloneContents();
+ if (documentFragment.textContent.length === 0) return null;
+
+ return documentFragment;
+ };
+
+ w.gl.utils.insertText = (target, text) => {
+ // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
+
+ const selectionStart = target.selectionStart;
+ const selectionEnd = target.selectionEnd;
+ const value = target.value;
+
+ const textBefore = value.substring(0, selectionStart);
+ const textAfter = value.substring(selectionEnd, value.length);
+ const newText = textBefore + text + textAfter;
+
+ target.value = newText;
+ target.selectionStart = target.selectionEnd = selectionStart + text.length;
+
+ // Trigger autosave
+ $(target).trigger('input');
+
+ // Trigger autosize
+ var event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ target.dispatchEvent(event);
+ };
+
+ w.gl.utils.nodeMatchesSelector = (node, selector) => {
+ const matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector;
+
+ if (matches) {
+ return matches.call(node, selector);
+ }
+
+ // IE11 doesn't support `node.matches(selector)`
+
+ let parentNode = node.parentNode;
+ if (!parentNode) {
+ parentNode = document.createElement('div');
+ node = node.cloneNode(true);
+ parentNode.appendChild(node);
+ }
+
+ const matchingNodes = parentNode.querySelectorAll(selector);
+ return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
+ };
+
+ /**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+ };
+
+ /**
+ * Parses pagination object string values into numbers.
+ *
+ * @param {Object} paginationInformation
+ * @returns {Object}
+ */
+ w.gl.utils.parseIntPagination = paginationInformation => ({
+ perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
+ page: parseInt(paginationInformation['X-PAGE'], 10),
+ total: parseInt(paginationInformation['X-TOTAL'], 10),
+ totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
+ nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
+ previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
+ });
+
+ /**
+ * Updates the search parameter of a URL given the parameter and values provided.
+ *
+ * If no search params are present we'll add it.
+ * If param for page is already present, we'll update it
+ * If there are params but not for the given one, we'll add it at the end.
+ * Returns the new search parameters.
+ *
+ * @param {String} param
+ * @param {Number|String|Undefined|Null} value
+ * @return {String}
+ */
+ w.gl.utils.setParamInURL = (param, value) => {
+ let search;
+ const locationSearch = window.location.search;
+
+ if (locationSearch.length === 0) {
+ search = `?${param}=${value}`;
+ }
+
+ if (locationSearch.indexOf(param) !== -1) {
+ const regex = new RegExp(param + '=\\d');
+ search = locationSearch.replace(regex, `${param}=${value}`);
+ }
+
+ if (locationSearch.length && locationSearch.indexOf(param) === -1) {
+ search = `${locationSearch}&${param}=${value}`;
+ }
+
+ return search;
+ };
+
+ /**
+ * Converts permission provided as strings to booleans.
+ *
+ * @param {String} string
+ * @returns {Boolean}
+ */
+ w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
+
+ /**
+ * Back Off exponential algorithm
+ * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
+ *
+ * @param {Function<next, stop>} fn function to be called
+ * @param {Number} timeout
+ * @return {Promise<Any, Error>}
+ * @example
+ * ```
+ * backOff(function (next, stop) {
+ * // Let's perform this function repeatedly for 60s or for the timeout provided.
+ *
+ * ourFunction()
+ * .then(function (result) {
+ * // continue if result is not what we need
+ * next();
+ *
+ * // when result is what we need let's stop with the repetions and jump out of the cycle
+ * stop(result);
+ * })
+ * .catch(function (error) {
+ * // if there is an error, we need to stop this with an error.
+ * stop(error);
+ * })
+ * }, 60000)
+ * .then(function (result) {})
+ * .catch(function (error) {
+ * // deal with errors passed to stop()
+ * })
+ * ```
+ */
+ w.gl.utils.backOff = (fn, timeout = 60000) => {
+ const maxInterval = 32000;
+ let nextInterval = 2000;
+
+ const startTime = Date.now();
+
+ return new Promise((resolve, reject) => {
+ const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+
+ const next = () => {
+ if (Date.now() - startTime < timeout) {
+ setTimeout(fn.bind(null, next, stop), nextInterval);
+ nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
+ } else {
+ reject(new Error('BACKOFF_TIMEOUT'));
+ }
+ };
+
+ fn(next, stop);
+ });
+ };
+ })(window);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
deleted file mode 100644
index 45a1d90a9d9..00000000000
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ /dev/null
@@ -1,300 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
-(function() {
- (function(w) {
- var base;
- w.gl || (w.gl = {});
- (base = w.gl).utils || (base.utils = {});
- w.gl.utils.isInGroupsPage = function() {
- return gl.utils.getPagePath() === 'groups';
- };
- w.gl.utils.isInProjectPage = function() {
- return gl.utils.getPagePath() === 'projects';
- };
- w.gl.utils.getProjectSlug = function() {
- if (this.isInProjectPage()) {
- return $('body').data('project');
- } else {
- return null;
- }
- };
- w.gl.utils.getGroupSlug = function() {
- if (this.isInGroupsPage()) {
- return $('body').data('group');
- } else {
- return null;
- }
- };
-
- w.gl.utils.ajaxGet = function(url) {
- return $.ajax({
- type: "GET",
- url: url,
- dataType: "script"
- });
- };
-
- w.gl.utils.extractLast = function(term) {
- return this.split(term).pop();
- };
-
- w.gl.utils.rstrip = function rstrip(val) {
- if (val) {
- return val.replace(/\s+$/, '');
- } else {
- return val;
- }
- };
-
- w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
- event_name = event_name || 'input';
- var closest_submit, field, that;
- that = this;
- field = $(field_selector);
- closest_submit = field.closest('form').find(button_selector);
- if (this.rstrip(field.val()) === "") {
- closest_submit.disable();
- }
- return field.on(event_name, function() {
- if (that.rstrip($(this).val()) === "") {
- return closest_submit.disable();
- } else {
- return closest_submit.enable();
- }
- });
- };
-
- // automatically adjust scroll position for hash urls taking the height of the navbar into account
- // https://github.com/twitter/bootstrap/issues/1768
- w.gl.utils.handleLocationHash = function() {
- var hash = w.gl.utils.getLocationHash();
- if (!hash) return;
-
- // This is required to handle non-unicode characters in hash
- hash = decodeURIComponent(hash);
-
- // scroll to user-generated markdown anchor if we cannot find a match
- if (document.getElementById(hash) === null) {
- var target = document.getElementById('user-content-' + hash);
- if (target && target.scrollIntoView) {
- target.scrollIntoView(true);
- }
- } else {
- // only adjust for fixedTabs when not targeting user-generated content
- var fixedTabs = document.querySelector('.js-tabs-affix');
- if (fixedTabs) {
- window.scrollBy(0, -fixedTabs.offsetHeight);
- }
- }
- };
-
- // Check if element scrolled into viewport from above or below
- // Courtesy http://stackoverflow.com/a/7557433/414749
- w.gl.utils.isInViewport = function(el) {
- var rect = el.getBoundingClientRect();
-
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= window.innerHeight &&
- rect.right <= window.innerWidth
- );
- };
-
- gl.utils.getPagePath = function(index) {
- index = index || 0;
- return $('body').data('page').split(':')[index];
- };
-
- gl.utils.parseUrl = function (url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- };
-
- gl.utils.parseUrlPathname = function (url) {
- var parsedUrl = gl.utils.parseUrl(url);
- // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
- // We have to make sure we always have an absolute path.
- return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
- };
-
- gl.utils.getUrlParamsArray = function () {
- // We can trust that each param has one & since values containing & will be encoded
- // Remove the first character of search as it is always ?
- return window.location.search.slice(1).split('&');
- };
-
- gl.utils.isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
- gl.utils.isMetaClick = function(e) {
- // Identify following special clicks
- // 1) Cmd + Click on Mac (e.metaKey)
- // 2) Ctrl + Click on PC (e.ctrlKey)
- // 3) Middle-click or Mouse Wheel Click (e.which is 2)
- return e.metaKey || e.ctrlKey || e.which === 2;
- };
-
- gl.utils.scrollToElement = function($el) {
- var top = $el.offset().top;
- gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
-
- return $('body, html').animate({
- scrollTop: top - (gl.mrTabsHeight)
- }, 200);
- };
-
- /**
- this will take in the `name` of the param you want to parse in the url
- if the name does not exist this function will return `null`
- otherwise it will return the value of the param key provided
- */
- w.gl.utils.getParameterByName = (name) => {
- const url = window.location.href;
- name = name.replace(/[[\]]/g, '\\$&');
- const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
- const results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeURIComponent(results[2].replace(/\+/g, ' '));
- };
-
- w.gl.utils.getSelectedFragment = () => {
- const selection = window.getSelection();
- if (selection.rangeCount === 0) return null;
- const documentFragment = selection.getRangeAt(0).cloneContents();
- if (documentFragment.textContent.length === 0) return null;
-
- return documentFragment;
- };
-
- w.gl.utils.insertText = (target, text) => {
- // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
-
- const selectionStart = target.selectionStart;
- const selectionEnd = target.selectionEnd;
- const value = target.value;
-
- const textBefore = value.substring(0, selectionStart);
- const textAfter = value.substring(selectionEnd, value.length);
- const newText = textBefore + text + textAfter;
-
- target.value = newText;
- target.selectionStart = target.selectionEnd = selectionStart + text.length;
-
- // Trigger autosave
- $(target).trigger('input');
-
- // Trigger autosize
- var event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- target.dispatchEvent(event);
- };
-
- w.gl.utils.nodeMatchesSelector = (node, selector) => {
- const matches = Element.prototype.matches ||
- Element.prototype.matchesSelector ||
- Element.prototype.mozMatchesSelector ||
- Element.prototype.msMatchesSelector ||
- Element.prototype.oMatchesSelector ||
- Element.prototype.webkitMatchesSelector;
-
- if (matches) {
- return matches.call(node, selector);
- }
-
- // IE11 doesn't support `node.matches(selector)`
-
- let parentNode = node.parentNode;
- if (!parentNode) {
- parentNode = document.createElement('div');
- node = node.cloneNode(true);
- parentNode.appendChild(node);
- }
-
- const matchingNodes = parentNode.querySelectorAll(selector);
- return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
- };
-
- /**
- this will take in the headers from an API response and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
- */
- w.gl.utils.normalizeHeaders = (headers) => {
- const upperCaseHeaders = {};
-
- Object.keys(headers).forEach((e) => {
- upperCaseHeaders[e.toUpperCase()] = headers[e];
- });
-
- return upperCaseHeaders;
- };
-
- /**
- * Parses pagination object string values into numbers.
- *
- * @param {Object} paginationInformation
- * @returns {Object}
- */
- w.gl.utils.parseIntPagination = paginationInformation => ({
- perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
- page: parseInt(paginationInformation['X-PAGE'], 10),
- total: parseInt(paginationInformation['X-TOTAL'], 10),
- totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
- nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
- previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
- });
-
- /**
- * Transforms a DOMStringMap into a plain object.
- *
- * @param {DOMStringMap} DOMStringMapObject
- * @returns {Object}
- */
- w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
- acc[element] = DOMStringMapObject[element];
- return acc;
- }, {});
-
- /**
- * Updates the search parameter of a URL given the parameter and values provided.
- *
- * If no search params are present we'll add it.
- * If param for page is already present, we'll update it
- * If there are params but not for the given one, we'll add it at the end.
- * Returns the new search parameters.
- *
- * @param {String} param
- * @param {Number|String|Undefined|Null} value
- * @return {String}
- */
- w.gl.utils.setParamInURL = (param, value) => {
- let search;
- const locationSearch = window.location.search;
-
- if (locationSearch.length === 0) {
- search = `?${param}=${value}`;
- }
-
- if (locationSearch.indexOf(param) !== -1) {
- const regex = new RegExp(param + '=\\d');
- search = locationSearch.replace(regex, `${param}=${value}`);
- }
-
- if (locationSearch.length && locationSearch.indexOf(param) === -1) {
- search = `${locationSearch}&${param}=${value}`;
- }
-
- return search;
- };
-
- /**
- * Converts permission provided as strings to booleans.
- *
- * @param {String} string
- * @returns {Boolean}
- */
- w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
- })(window);
-}).call(window);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js
index 82dcbdc26c8..82dcbdc26c8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js.es6
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
new file mode 100644
index 00000000000..bc109a69c20
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -0,0 +1,10 @@
+/**
+ * exports HTTP status codes
+ */
+
+const statusCodes = {
+ NO_CONTENT: 204,
+ OK: 200,
+};
+
+module.exports = statusCodes;
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js
index ae397212e55..ae397212e55 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js.es6
+++ b/app/assets/javascripts/lib/utils/pretty_time.js
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 579d322e3fb..2e5f8a09fc1 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -65,9 +65,10 @@ require('vendor/latinise');
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine;
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
+ currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
@@ -82,7 +83,17 @@ require('vendor/latinise');
}
selectedSplit = selected.split('\n');
- startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
+
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
+
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) {
@@ -142,9 +153,8 @@ require('vendor/latinise');
}
};
gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, oldVal, selected, text;
+ var $textArea, selected, text;
$textArea = $(textArea);
- oldVal = $textArea.val();
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
new file mode 100644
index 00000000000..09c4261b318
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -0,0 +1,93 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ // Returns an array containing the value(s) of the
+ // of the key passed as an argument
+ w.gl.utils.getParameterValues = function(sParam) {
+ var i, sPageURL, sParameterName, sURLVariables, values;
+ sPageURL = decodeURIComponent(window.location.search.substring(1));
+ sURLVariables = sPageURL.split('&');
+ sParameterName = void 0;
+ values = [];
+ i = 0;
+ while (i < sURLVariables.length) {
+ sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] === sParam) {
+ values.push(sParameterName[1].replace(/\+/g, ' '));
+ }
+ i += 1;
+ }
+ return values;
+ };
+ // @param {Object} params - url keys and value to merge
+ // @param {String} url
+ w.gl.utils.mergeUrlParams = function(params, url) {
+ var lastChar, newUrl, paramName, paramValue, pattern;
+ newUrl = decodeURIComponent(url);
+ for (paramName in params) {
+ paramValue = params[paramName];
+ pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
+ if (paramValue == null) {
+ newUrl = newUrl.replace(pattern, '');
+ } else if (url.search(pattern) !== -1) {
+ newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
+ } else {
+ newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
+ }
+ }
+ // Remove a trailing ampersand
+ lastChar = newUrl[newUrl.length - 1];
+ if (lastChar === '&') {
+ newUrl = newUrl.slice(0, -1);
+ }
+ return newUrl;
+ };
+ // removes parameter query string from url. returns the modified url
+ w.gl.utils.removeParamQueryString = function(url, param) {
+ var urlVariables, variables;
+ url = decodeURIComponent(url);
+ urlVariables = url.split('&');
+ return ((function() {
+ var j, len, results;
+ results = [];
+ for (j = 0, len = urlVariables.length; j < len; j += 1) {
+ variables = urlVariables[j];
+ if (variables.indexOf(param) === -1) {
+ results.push(variables);
+ }
+ }
+ return results;
+ })()).join('&');
+ };
+ w.gl.utils.removeParams = (params) => {
+ const url = new URL(window.location.href);
+ params.forEach((param) => {
+ url.search = w.gl.utils.removeParamQueryString(url.search, param);
+ });
+ return url.href;
+ };
+ w.gl.utils.getLocationHash = function(url) {
+ var hashIndex;
+ if (typeof url === 'undefined') {
+ // Note: We can't use window.location.hash here because it's
+ // not consistent across browsers - Firefox will pre-decode it
+ url = window.location.href;
+ }
+ hashIndex = url.indexOf('#');
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+ };
+
+ w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+
+ w.gl.utils.visitUrl = (url) => {
+ document.location.href = url;
+ };
+ })(window);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6
deleted file mode 100644
index 1bc81d2e4a4..00000000000
--- a/app/assets/javascripts/lib/utils/url_utility.js.es6
+++ /dev/null
@@ -1,86 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- // Returns an array containing the value(s) of the
- // of the key passed as an argument
- w.gl.utils.getParameterValues = function(sParam) {
- var i, sPageURL, sParameterName, sURLVariables, values;
- sPageURL = decodeURIComponent(window.location.search.substring(1));
- sURLVariables = sPageURL.split('&');
- sParameterName = void 0;
- values = [];
- i = 0;
- while (i < sURLVariables.length) {
- sParameterName = sURLVariables[i].split('=');
- if (sParameterName[0] === sParam) {
- values.push(sParameterName[1].replace(/\+/g, ' '));
- }
- i += 1;
- }
- return values;
- };
- // @param {Object} params - url keys and value to merge
- // @param {String} url
- w.gl.utils.mergeUrlParams = function(params, url) {
- var lastChar, newUrl, paramName, paramValue, pattern;
- newUrl = decodeURIComponent(url);
- for (paramName in params) {
- paramValue = params[paramName];
- pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
- if (paramValue == null) {
- newUrl = newUrl.replace(pattern, '');
- } else if (url.search(pattern) !== -1) {
- newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
- } else {
- newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
- }
- }
- // Remove a trailing ampersand
- lastChar = newUrl[newUrl.length - 1];
- if (lastChar === '&') {
- newUrl = newUrl.slice(0, -1);
- }
- return newUrl;
- };
- // removes parameter query string from url. returns the modified url
- w.gl.utils.removeParamQueryString = function(url, param) {
- var urlVariables, variables;
- url = decodeURIComponent(url);
- urlVariables = url.split('&');
- return ((function() {
- var j, len, results;
- results = [];
- for (j = 0, len = urlVariables.length; j < len; j += 1) {
- variables = urlVariables[j];
- if (variables.indexOf(param) === -1) {
- results.push(variables);
- }
- }
- return results;
- })()).join('&');
- };
- w.gl.utils.getLocationHash = function(url) {
- var hashIndex;
- if (typeof url === 'undefined') {
- // Note: We can't use window.location.hash here because it's
- // not consistent across browsers - Firefox will pre-decode it
- url = window.location.href;
- }
- hashIndex = url.indexOf('#');
- return hashIndex === -1 ? null : url.substring(hashIndex + 1);
- };
-
- w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
-
- w.gl.utils.visitUrl = (url) => {
- document.location.href = url;
- };
- })(window);
-}).call(window);
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
deleted file mode 100644
index 49babdea2e1..00000000000
--- a/app/assets/javascripts/lib/vue_resource.js.es6
+++ /dev/null
@@ -1,2 +0,0 @@
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 966fcd8ec47..1821ca18053 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo');
}
LineHighlighter.prototype.bindEvents = function() {
- $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
- // While it may seem odd to bind to the mousedown event and then throw away
- // the click event, there is a method to our madness.
- //
- // If not done this way, the line number anchor will sometimes keep its
- // active state even when the event is cancelled, resulting in an ugly border
- // around the link and/or a persisted underline text decoration.
- $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
- event.preventDefault();
- event.stopPropagation();
- });
+ $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
};
LineHighlighter.prototype.clickHandler = function(event) {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
new file mode 100644
index 00000000000..81d5748191d
--- /dev/null
+++ b/app/assets/javascripts/main.js
@@ -0,0 +1,384 @@
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* global Aside */
+
+import jQuery from 'jquery';
+import _ from 'underscore';
+import Cookies from 'js-cookie';
+import Pikaday from 'pikaday';
+import Dropzone from 'dropzone';
+import Sortable from 'vendor/Sortable';
+
+// libraries with import side-effects
+import 'mousetrap';
+import 'mousetrap/plugins/pause/mousetrap-pause';
+import 'vendor/fuzzaldrin-plus';
+
+// extensions
+import './extensions/array';
+
+// expose common libraries as globals (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+window._ = _;
+window.Cookies = Cookies;
+window.Pikaday = Pikaday;
+window.Dropzone = Dropzone;
+window.Sortable = Sortable;
+
+// shortcuts
+import './shortcuts';
+import './shortcuts_blob';
+import './shortcuts_dashboard_navigation';
+import './shortcuts_navigation';
+import './shortcuts_find_file';
+import './shortcuts_issuable';
+import './shortcuts_network';
+
+// behaviors
+import './behaviors/autosize';
+import './behaviors/details_behavior';
+import './behaviors/quick_submit';
+import './behaviors/requires_input';
+import './behaviors/toggler_behavior';
+import './behaviors/bind_in_out';
+import { installGlEmojiElement } from './behaviors/gl_emoji';
+installGlEmojiElement();
+
+// blob
+import './blob/blob_ci_yaml';
+import './blob/blob_dockerfile_selector';
+import './blob/blob_dockerfile_selectors';
+import './blob/blob_file_dropzone';
+import './blob/blob_gitignore_selector';
+import './blob/blob_gitignore_selectors';
+import './blob/blob_license_selector';
+import './blob/blob_license_selectors';
+import './blob/template_selector';
+import './blob/create_branch_dropdown';
+import './blob/target_branch_dropdown';
+
+// templates
+import './templates/issuable_template_selector';
+import './templates/issuable_template_selectors';
+
+// commit
+import './commit/file';
+import './commit/image_file';
+
+// lib/utils
+import './lib/utils/animate';
+import './lib/utils/bootstrap_linked_tabs';
+import './lib/utils/common_utils';
+import './lib/utils/datetime_utility';
+import './lib/utils/notify';
+import './lib/utils/pretty_time';
+import './lib/utils/text_utility';
+import './lib/utils/type_utility';
+import './lib/utils/url_utility';
+
+// u2f
+import './u2f/authenticate';
+import './u2f/error';
+import './u2f/register';
+import './u2f/util';
+
+// droplab
+import './droplab/droplab';
+import './droplab/droplab_ajax';
+import './droplab/droplab_ajax_filter';
+import './droplab/droplab_filter';
+
+// everything else
+import './abuse_reports';
+import './activities';
+import './admin';
+import './ajax_loading_spinner';
+import './api';
+import './aside';
+import './autosave';
+import AwardsHandler from './awards_handler';
+import './breakpoints';
+import './broadcast_message';
+import './build';
+import './build_artifacts';
+import './build_variables';
+import './ci_lint_editor';
+import './commit';
+import './commits';
+import './compare';
+import './compare_autocomplete';
+import './confirm_danger_modal';
+import './copy_as_gfm';
+import './copy_to_clipboard';
+import './create_label';
+import './diff';
+import './dispatcher';
+import './dropzone_input';
+import './due_date_select';
+import './files_comment_button';
+import './flash';
+import './gfm_auto_complete';
+import './gl_dropdown';
+import './gl_field_error';
+import './gl_field_errors';
+import './gl_form';
+import './group_avatar';
+import './group_label_subscription';
+import './groups_select';
+import './header';
+import './importer_status';
+import './issuable';
+import './issuable_context';
+import './issuable_form';
+import './issue';
+import './issue_status_select';
+import './issues_bulk_assignment';
+import './label_manager';
+import './labels';
+import './labels_select';
+import './layout_nav';
+import './line_highlighter';
+import './logo';
+import './member_expiration_date';
+import './members';
+import './merge_request';
+import './merge_request_tabs';
+import './merge_request_widget';
+import './merged_buttons';
+import './milestone';
+import './milestone_select';
+import './mini_pipeline_graph_dropdown';
+import './namespace_select';
+import './new_branch_form';
+import './new_commit_form';
+import './notes';
+import './notifications_dropdown';
+import './notifications_form';
+import './pager';
+import './pipelines';
+import './preview_markdown';
+import './project';
+import './project_avatar';
+import './project_find_file';
+import './project_fork';
+import './project_import';
+import './project_label_subscription';
+import './project_new';
+import './project_select';
+import './project_show';
+import './project_variables';
+import './projects_list';
+import './render_gfm';
+import './render_math';
+import './right_sidebar';
+import './search';
+import './search_autocomplete';
+import './signin_tabs_memoizer';
+import './single_file_diff';
+import './smart_interval';
+import './snippets_list';
+import './star';
+import './subbable_resource';
+import './subscription';
+import './subscription_select';
+import './syntax_highlight';
+import './task_list';
+import './todos';
+import './tree';
+import './user';
+import './user_tabs';
+import './username_validator';
+import './users_select';
+import './version_check_image';
+import './visibility_select';
+import './wikis';
+import './zen_mode';
+
+document.addEventListener('beforeunload', function () {
+ // Unbind scroll events
+ $(document).off('scroll');
+ // Close any open tooltips
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+});
+
+window.addEventListener('hashchange', gl.utils.handleLocationHash);
+window.addEventListener('load', function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ gl.utils.handleLocationHash();
+}, false);
+
+$(function () {
+ var $body = $('body');
+ var $document = $(document);
+ var $window = $(window);
+ var $sidebarGutterToggle = $('.js-sidebar-toggle');
+ var $flash = $('.flash-container');
+ var bootstrapBreakpoint = bp.getBreakpointSize();
+ var fitSidebarForSize;
+
+ // Set the default path for all cookies to GitLab's root directory
+ Cookies.defaults.path = gon.relative_url_root || '/';
+
+ // `hashchange` is not triggered when link target is already in window.location
+ $body.on('click', 'a[href^="#"]', function() {
+ var href = this.getAttribute('href');
+ if (href.substr(1) === gl.utils.getLocationHash()) {
+ setTimeout(gl.utils.handleLocationHash, 1);
+ }
+ });
+
+ // prevent default action for disabled buttons
+ $('.btn').click(function(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+ });
+
+ $('.js-select-on-focus').on('focusin', function () {
+ return $(this).select().one('mouseup', function (e) {
+ return e.preventDefault();
+ });
+ // Click a .js-select-on-focus field, select the contents
+ // Prevent a mouseup event from deselecting the input
+ });
+ $('.remove-row').bind('ajax:success', function () {
+ $(this).tooltip('destroy')
+ .closest('li')
+ .fadeOut();
+ });
+ $('.js-remove-tr').bind('ajax:before', function () {
+ return $(this).hide();
+ });
+ $('.js-remove-tr').bind('ajax:success', function () {
+ return $(this).closest('tr').fadeOut();
+ });
+ $('select.select2').select2({
+ width: 'resolve',
+ // Initialize select2 selects
+ dropdownAutoWidth: true
+ });
+ $('.js-select2').bind('select2-close', function () {
+ return setTimeout((function () {
+ $('.select2-container-active').removeClass('select2-container-active');
+ return $(':focus').blur();
+ }), 1);
+ // Close select2 on escape
+ });
+ // Initialize tooltips
+ $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
+ $body.tooltip({
+ selector: '.has-tooltip, [data-toggle="tooltip"]',
+ placement: function (tip, el) {
+ return $(el).data('placement') || 'bottom';
+ }
+ });
+ $('.trigger-submit').on('change', function () {
+ return $(this).parents('form').submit();
+ // Form submitter
+ });
+ gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ // Flash
+ if ($flash.length > 0) {
+ $flash.click(function () {
+ return $(this).fadeOut();
+ });
+ $flash.show();
+ }
+ // Disable form buttons while a form is submitting
+ $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
+ var buttons;
+ buttons = $('[type="submit"]', this);
+ switch (e.type) {
+ case 'ajax:beforeSend':
+ case 'submit':
+ return buttons.disable();
+ default:
+ return buttons.enable();
+ }
+ });
+ $(document).ajaxError(function (e, xhrObj) {
+ var ref = xhrObj.status;
+ if (xhrObj.status === 401) {
+ return new Flash('You need to be logged in.', 'alert');
+ } else if (ref === 404 || ref === 500) {
+ return new Flash('Something went wrong on our end.', 'alert');
+ }
+ });
+ $('.account-box').hover(function () {
+ // Show/Hide the profile menu when hovering the account box
+ return $(this).toggleClass('hover');
+ });
+ $document.on('click', '.diff-content .js-show-suppressed-diff', function () {
+ var $container;
+ $container = $(this).parent();
+ $container.next('table').show();
+ return $container.remove();
+ // Commit show suppressed diff
+ });
+ $('.navbar-toggle').on('click', function () {
+ $('.header-content .title').toggle();
+ $('.header-content .header-logo').toggle();
+ $('.header-content .navbar-collapse').toggle();
+ return $('.navbar-toggle').toggleClass('active');
+ });
+ // Show/hide comments on diff
+ $body.on('click', '.js-toggle-diff-comments', function (e) {
+ var $this = $(this);
+ var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+ $this.toggleClass('active');
+ if ($this.hasClass('active')) {
+ notesHolders.show().find('.hide, .content').show();
+ } else {
+ notesHolders.hide().find('.content').hide();
+ }
+ $(document).trigger('toggle.comments');
+ return e.preventDefault();
+ });
+ $document.off('click', '.js-confirm-danger');
+ $document.on('click', '.js-confirm-danger', function (e) {
+ var btn = $(e.target);
+ var form = btn.closest('form');
+ var text = btn.data('confirm-danger-message');
+ e.preventDefault();
+ return new ConfirmDangerModal(form, text);
+ });
+ $('input[type="search"]').each(function () {
+ var $this = $(this);
+ $this.attr('value', $this.val());
+ });
+ $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
+ var $this;
+ $this = $(this);
+ return $this.attr('value', $this.val());
+ });
+ $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
+ var $gutterIcon;
+ if (breakpoint === 'sm' || breakpoint === 'xs') {
+ $gutterIcon = $sidebarGutterToggle.find('i');
+ if ($gutterIcon.hasClass('fa-angle-double-right')) {
+ return $sidebarGutterToggle.trigger('click');
+ }
+ }
+ });
+ fitSidebarForSize = function () {
+ var oldBootstrapBreakpoint;
+ oldBootstrapBreakpoint = bootstrapBreakpoint;
+ bootstrapBreakpoint = bp.getBreakpointSize();
+ if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
+ return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+ }
+ };
+ $window.off('resize.app').on('resize.app', function () {
+ return fitSidebarForSize();
+ });
+ gl.awardsHandler = new AwardsHandler();
+ new Aside();
+
+ gl.utils.initTimeagoTimeout();
+});
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js
index 129d2dc5f0a..129d2dc5f0a 100644
--- a/app/assets/javascripts/member_expiration_date.js.es6
+++ b/app/assets/javascripts/member_expiration_date.js
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js
index e3f367a11eb..e3f367a11eb 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index c7e78fed8fe..c7e78fed8fe 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index 240c8f98932..240c8f98932 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index 97753c50b60..97753c50b60 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
index c012b77e0bf..c012b77e0bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 74587df22c5..74587df22c5 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 653e52fb6bf..653e52fb6bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
index 53e000d7e9e..53e000d7e9e 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
index 0f475f62ee6..0f475f62ee6 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js
index 190336dbd20..190336dbd20 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
new file mode 100644
index 00000000000..0e2af3df071
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -0,0 +1,296 @@
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
+/* global notify */
+/* global notifyPermissions */
+/* global merge_request_widget */
+
+import './smart_interval';
+import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
+
+((global) => {
+ var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
+
+ const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
+ <div class="ci_widget ci-success">
+ <%= ci_success_icon %>
+ <span>
+ Deployed to
+ <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
+ <%- name %>
+ </a>
+ <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
+ <%- deployed_at %>
+ </span>
+ <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
+ <i class="fa fa-external-link"></i>
+ View on <%- external_url_formatted %>
+ </a>
+ </span>
+ <span class="stop-env-container js-stop-env-link">
+ <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
+ <i class="fa fa-stop-circle-o"/>
+ Stop environment
+ </a>
+ </span>
+ </div>
+ </div>`;
+
+ global.MergeRequestWidget = (function() {
+ function MergeRequestWidget(opts) {
+ // Initialize MergeRequestWidget behavior
+ //
+ // check_enable - Boolean, whether to check automerge status
+ // merge_check_url - String, URL to use to check automerge status
+ // ci_status_url - String, URL to use to check CI status
+ //
+ this.opts = opts;
+ this.$widgetBody = $('.mr-widget-body');
+ $('#modal_merge_info').modal({
+ show: false
+ });
+ this.clearEventListeners();
+ this.addEventListeners();
+ this.getCIStatus(false);
+ this.retrieveSuccessIcon();
+
+ this.initMiniPipelineGraph();
+
+ this.ciStatusInterval = new global.SmartInterval({
+ callback: this.getCIStatus.bind(this, true),
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ this.ciEnvironmentStatusInterval = new global.SmartInterval({
+ callback: this.getCIEnvironmentsStatus.bind(this),
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
+
+ notifyPermissions();
+ }
+
+ MergeRequestWidget.prototype.clearEventListeners = function() {
+ return $(document).off('DOMContentLoaded');
+ };
+
+ MergeRequestWidget.prototype.addEventListeners = function() {
+ var allowedPages;
+ allowedPages = ['show', 'commits', 'pipelines', 'changes'];
+ $(document).on('DOMContentLoaded', (function(_this) {
+ return function() {
+ var page;
+ page = $('body').data('page').split(':').last();
+ if (allowedPages.indexOf(page) === -1) {
+ return _this.clearEventListeners();
+ }
+ };
+ })(this));
+ };
+
+ MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
+ const $ciSuccessIcon = $('.js-success-icon');
+ this.$ciSuccessIcon = $ciSuccessIcon.html();
+ $ciSuccessIcon.remove();
+ };
+
+ MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
+ if (deleteSourceBranch == null) {
+ deleteSourceBranch = false;
+ }
+ return $.ajax({
+ type: 'GET',
+ url: $('.merge-request').data('url'),
+ success: (function(_this) {
+ return function(data) {
+ var callback, urlSuffix;
+ if (data.state === "merged") {
+ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
+ return window.location.href = window.location.pathname + urlSuffix;
+ } else if (data.merge_error) {
+ return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
+ } else {
+ callback = function() {
+ return merge_request_widget.mergeInProgress(deleteSourceBranch);
+ };
+ return setTimeout(callback, 2000);
+ }
+ };
+ })(this),
+ dataType: 'json'
+ });
+ };
+
+ MergeRequestWidget.prototype.cancelPolling = function () {
+ this.ciStatusInterval.cancel();
+ this.ciEnvironmentStatusInterval.cancel();
+ };
+
+ MergeRequestWidget.prototype.getMergeStatus = function() {
+ return $.get(this.opts.merge_check_url, (data) => {
+ var $html = $(data);
+ this.updateMergeButton(this.status, this.hasCi, $html);
+ $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
+ $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
+ });
+ };
+
+ MergeRequestWidget.prototype.ciLabelForStatus = function(status) {
+ switch (status) {
+ case 'success':
+ return 'passed';
+ case 'success_with_warnings':
+ return 'passed with warnings';
+ default:
+ return status;
+ }
+ };
+
+ MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
+ var _this;
+ _this = this;
+ $('.ci-widget-fetching').show();
+ return $.getJSON(this.opts.ci_status_url, (function(_this) {
+ return function(data) {
+ var message, status, title;
+ _this.status = data.status;
+ _this.hasCi = data.has_ci;
+ _this.updateMergeButton(_this.status, _this.hasCi);
+ if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
+ if (data.status !== _this.opts.ci_status ||
+ data.sha !== _this.opts.ci_sha ||
+ data.pipeline !== _this.opts.ci_pipeline) {
+ _this.opts.ci_status = data.status;
+ _this.showCIStatus(data.status);
+ if (data.coverage) {
+ _this.showCICoverage(data.coverage);
+ }
+ if (data.pipeline) {
+ _this.opts.ci_pipeline = data.pipeline;
+ _this.updatePipelineUrls(data.pipeline);
+ }
+ if (data.sha) {
+ _this.opts.ci_sha = data.sha;
+ _this.updateCommitUrls(data.sha);
+ }
+ if (showNotification && data.status) {
+ status = _this.ciLabelForStatus(data.status);
+ if (status === "preparing") {
+ title = _this.opts.ci_title.preparing;
+ status = status.charAt(0).toUpperCase() + status.slice(1);
+ message = _this.opts.ci_message.preparing.replace('{{status}}', status);
+ } else {
+ title = _this.opts.ci_title.normal;
+ message = _this.opts.ci_message.normal.replace('{{status}}', status);
+ }
+ title = title.replace('{{status}}', status);
+ message = message.replace('{{sha}}', data.sha);
+ message = message.replace('{{title}}', data.title);
+ notify(title, message, _this.opts.gitlab_icon, function() {
+ this.close();
+ });
+ }
+ }
+ };
+ })(this));
+ };
+
+ MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
+ $.getJSON(this.opts.ci_environments_status_url, (environments) => {
+ if (environments && environments.length) this.renderEnvironments(environments);
+ });
+ };
+
+ MergeRequestWidget.prototype.renderEnvironments = function(environments) {
+ for (let i = 0; i < environments.length; i += 1) {
+ const environment = environments[i];
+ if ($(`.mr-state-widget #${environment.id}`).length) return;
+ const $template = $(DEPLOYMENT_TEMPLATE);
+ if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
+
+ if (!environment.stop_url) {
+ $('.js-stop-env-link', $template).remove();
+ }
+
+ if (environment.deployed_at && environment.deployed_at_formatted) {
+ environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
+ } else {
+ $('.js-environment-timeago', $template).remove();
+ environment.name += '.';
+ }
+ environment.ci_success_icon = this.$ciSuccessIcon;
+ const templateString = _.unescape($template[0].outerHTML);
+ const template = _.template(templateString)(environment);
+ this.$widgetBody.before(template);
+ }
+ };
+
+ MergeRequestWidget.prototype.showCIStatus = function(state) {
+ var allowed_states;
+ if (state == null) {
+ return;
+ }
+ $('.ci_widget').hide();
+ $('.ci_widget.ci-' + state).show();
+
+ this.initMiniPipelineGraph();
+ };
+
+ MergeRequestWidget.prototype.showCICoverage = function(coverage) {
+ var text = `Coverage ${coverage}%`;
+ return $('.ci_widget:visible .ci-coverage').text(text);
+ };
+
+ MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) {
+ const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
+ let stateClass = 'btn-danger';
+ if (!hasCi) {
+ stateClass = 'btn-create';
+ } else if (indexOf.call(allowed_states, state) !== -1) {
+ switch (state) {
+ case "failed":
+ case "canceled":
+ case "not_found":
+ stateClass = 'btn-danger';
+ break;
+ case "running":
+ stateClass = 'btn-info';
+ break;
+ case "success":
+ case "success_with_warnings":
+ stateClass = 'btn-create';
+ }
+ } else {
+ $('.ci_widget.ci-error').show();
+ stateClass = 'btn-danger';
+ }
+
+ this.setMergeButtonClass(stateClass, $html);
+ };
+
+ MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) {
+ return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class);
+ };
+
+ MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
+ const pipelineUrl = this.opts.pipeline_path;
+ $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
+ };
+
+ MergeRequestWidget.prototype.updateCommitUrls = function(id) {
+ const commitsUrl = this.opts.commits_path;
+ $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
+ };
+
+ MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
+ new MiniPipelineGraph({
+ container: '.js-pipeline-inline-mr-widget-graph:visible',
+ }).bindEvents();
+ };
+
+ return MergeRequestWidget;
+ })();
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
deleted file mode 100644
index 88f08bbaa34..00000000000
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ /dev/null
@@ -1,285 +0,0 @@
-/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
-/* global notify */
-/* global notifyPermissions */
-/* global merge_request_widget */
-
-require('./smart_interval');
-
-((global) => {
- var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
-
- const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
- <div class="ci_widget ci-success">
- <%= ci_success_icon %>
- <span>
- Deployed to
- <a href="<%- url %>" target="_blank" class="environment">
- <%- name %>
- </a>
- <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
- <%- deployed_at %>
- </span>
- <a class="js-environment-link" href="<%- external_url %>" target="_blank">
- <i class="fa fa-external-link"></i>
- View on <%- external_url_formatted %>
- </a>
- </span>
- <span class="stop-env-container js-stop-env-link">
- <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
- <i class="fa fa-stop-circle-o"/>
- Stop environment
- </a>
- </span>
- </div>
- </div>`;
-
- global.MergeRequestWidget = (function() {
- function MergeRequestWidget(opts) {
- // Initialize MergeRequestWidget behavior
- //
- // check_enable - Boolean, whether to check automerge status
- // merge_check_url - String, URL to use to check automerge status
- // ci_status_url - String, URL to use to check CI status
- //
- this.opts = opts;
- this.$widgetBody = $('.mr-widget-body');
- $('#modal_merge_info').modal({
- show: false
- });
- this.clearEventListeners();
- this.addEventListeners();
- this.getCIStatus(false);
- this.retrieveSuccessIcon();
-
- this.initMiniPipelineGraph();
-
- this.ciStatusInterval = new global.SmartInterval({
- callback: this.getCIStatus.bind(this, true),
- startingInterval: 10000,
- maxInterval: 30000,
- hiddenInterval: 120000,
- incrementByFactorOf: 5000,
- });
- this.ciEnvironmentStatusInterval = new global.SmartInterval({
- callback: this.getCIEnvironmentsStatus.bind(this),
- startingInterval: 30000,
- maxInterval: 120000,
- hiddenInterval: 240000,
- incrementByFactorOf: 15000,
- immediateExecution: true,
- });
-
- notifyPermissions();
- }
-
- MergeRequestWidget.prototype.clearEventListeners = function() {
- return $(document).off('DOMContentLoaded');
- };
-
- MergeRequestWidget.prototype.addEventListeners = function() {
- var allowedPages;
- allowedPages = ['show', 'commits', 'pipelines', 'changes'];
- $(document).on('DOMContentLoaded', (function(_this) {
- return function() {
- var page;
- page = $('body').data('page').split(':').last();
- if (allowedPages.indexOf(page) < 0) {
- return _this.clearEventListeners();
- }
- };
- })(this));
- };
-
- MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
- const $ciSuccessIcon = $('.js-success-icon');
- this.$ciSuccessIcon = $ciSuccessIcon.html();
- $ciSuccessIcon.remove();
- };
-
- MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
- if (deleteSourceBranch == null) {
- deleteSourceBranch = false;
- }
- return $.ajax({
- type: 'GET',
- url: $('.merge-request').data('url'),
- success: (function(_this) {
- return function(data) {
- var callback, urlSuffix;
- if (data.state === "merged") {
- urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
- return window.location.href = window.location.pathname + urlSuffix;
- } else if (data.merge_error) {
- return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
- } else {
- callback = function() {
- return merge_request_widget.mergeInProgress(deleteSourceBranch);
- };
- return setTimeout(callback, 2000);
- }
- };
- })(this),
- dataType: 'json'
- });
- };
-
- MergeRequestWidget.prototype.cancelPolling = function () {
- this.ciStatusInterval.cancel();
- this.ciEnvironmentStatusInterval.cancel();
- };
-
- MergeRequestWidget.prototype.getMergeStatus = function() {
- return $.get(this.opts.merge_check_url, function(data) {
- var $html = $(data);
- $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
- $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
- });
- };
-
- MergeRequestWidget.prototype.ciLabelForStatus = function(status) {
- switch (status) {
- case 'success':
- return 'passed';
- case 'success_with_warnings':
- return 'passed with warnings';
- default:
- return status;
- }
- };
-
- MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
- var _this;
- _this = this;
- $('.ci-widget-fetching').show();
- return $.getJSON(this.opts.ci_status_url, (function(_this) {
- return function(data) {
- var message, status, title;
- if (!data.status) {
- return;
- }
- if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (data.status !== _this.opts.ci_status ||
- data.sha !== _this.opts.ci_sha ||
- data.pipeline !== _this.opts.ci_pipeline) {
- _this.opts.ci_status = data.status;
- _this.showCIStatus(data.status);
- if (data.coverage) {
- _this.showCICoverage(data.coverage);
- }
- if (data.pipeline) {
- _this.opts.ci_pipeline = data.pipeline;
- _this.updatePipelineUrls(data.pipeline);
- }
- if (data.sha) {
- _this.opts.ci_sha = data.sha;
- _this.updateCommitUrls(data.sha);
- }
- if (showNotification) {
- status = _this.ciLabelForStatus(data.status);
- if (status === "preparing") {
- title = _this.opts.ci_title.preparing;
- status = status.charAt(0).toUpperCase() + status.slice(1);
- message = _this.opts.ci_message.preparing.replace('{{status}}', status);
- } else {
- title = _this.opts.ci_title.normal;
- message = _this.opts.ci_message.normal.replace('{{status}}', status);
- }
- title = title.replace('{{status}}', status);
- message = message.replace('{{sha}}', data.sha);
- message = message.replace('{{title}}', data.title);
- notify(title, message, _this.opts.gitlab_icon, function() {
- this.close();
- });
- }
- }
- };
- })(this));
- };
-
- MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
- $.getJSON(this.opts.ci_environments_status_url, (environments) => {
- if (environments && environments.length) this.renderEnvironments(environments);
- });
- };
-
- MergeRequestWidget.prototype.renderEnvironments = function(environments) {
- for (let i = 0; i < environments.length; i += 1) {
- const environment = environments[i];
- if ($(`.mr-state-widget #${environment.id}`).length) return;
- const $template = $(DEPLOYMENT_TEMPLATE);
- if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
-
- if (!environment.stop_url) {
- $('.js-stop-env-link', $template).remove();
- }
-
- if (environment.deployed_at && environment.deployed_at_formatted) {
- environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
- } else {
- $('.js-environment-timeago', $template).remove();
- environment.name += '.';
- }
- environment.ci_success_icon = this.$ciSuccessIcon;
- const templateString = _.unescape($template[0].outerHTML);
- const template = _.template(templateString)(environment);
- this.$widgetBody.before(template);
- }
- };
-
- MergeRequestWidget.prototype.showCIStatus = function(state) {
- var allowed_states;
- if (state == null) {
- return;
- }
- $('.ci_widget').hide();
- allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
- if (indexOf.call(allowed_states, state) >= 0) {
- $('.ci_widget.ci-' + state).show();
- switch (state) {
- case "failed":
- case "canceled":
- case "not_found":
- this.setMergeButtonClass('btn-danger');
- break;
- case "running":
- this.setMergeButtonClass('btn-info');
- break;
- case "success":
- case "success_with_warnings":
- this.setMergeButtonClass('btn-create');
- }
- } else {
- $('.ci_widget.ci-error').show();
- this.setMergeButtonClass('btn-danger');
- }
- };
-
- MergeRequestWidget.prototype.showCICoverage = function(coverage) {
- var text;
- text = 'Coverage ' + coverage + '%';
- return $('.ci_widget:visible .ci-coverage').text(text);
- };
-
- MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) {
- return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
- };
-
- MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
- const pipelineUrl = this.opts.pipeline_path;
- $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
- };
-
- MergeRequestWidget.prototype.updateCommitUrls = function(id) {
- const commitsUrl = this.opts.commits_path;
- $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
- };
-
- MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-inline-mr-widget-graph:visible',
- }).bindEvents();
- };
-
- return MergeRequestWidget;
- })();
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js b/app/assets/javascripts/merge_request_widget/ci_bundle.js
new file mode 100644
index 00000000000..21d7c3e168e
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js
@@ -0,0 +1,53 @@
+/* global merge_request_widget */
+
+(() => {
+ $(() => {
+ /* TODO: This needs a better home, or should be refactored. It was previously contained
+ * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
+ * but Vue chokes on script tags and prevents their execution. So it was moved here
+ * temporarily.
+ * */
+
+ $(document)
+ .off('ajax:send', '.accept-mr-form')
+ .on('ajax:send', '.accept-mr-form', () => {
+ $('.accept-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.accept-merge-request')
+ .on('click', '.accept-merge-request', () => {
+ $('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+ });
+
+ $(document)
+ .off('click', '.merge-when-pipeline-succeeds')
+ .on('click', '.merge-when-pipeline-succeeds', () => {
+ $('#merge_when_pipeline_succeeds').val('1');
+ });
+
+ $(document)
+ .off('click', '.js-merge-dropdown a')
+ .on('click', '.js-merge-dropdown a', (e) => {
+ e.preventDefault();
+ $(e.target).closest('form').submit();
+ });
+ if ($('.rebase-in-progress').length) {
+ merge_request_widget.rebaseInProgress();
+ } else if ($('.rebase-mr-form').length) {
+ $(document)
+ .off('ajax:send', '.rebase-mr-form')
+ .on('ajax:send', '.rebase-mr-form', () => {
+ $('.rebase-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.js-rebase-button')
+ .on('click', '.js-rebase-button', () => {
+ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
+ });
+ } else {
+ setTimeout(() => merge_request_widget.getMergeStatus(), 200);
+ }
+ });
+})();
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
deleted file mode 100644
index 5840916846b..00000000000
--- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
+++ /dev/null
@@ -1,53 +0,0 @@
-/* global merge_request_widget */
-
-(() => {
- $(() => {
- /* TODO: This needs a better home, or should be refactored. It was previously contained
- * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
- * but Vue chokes on script tags and prevents their execution. So it was moved here
- * temporarily.
- * */
-
- $(document)
- .off('ajax:send', '.accept-mr-form')
- .on('ajax:send', '.accept-mr-form', () => {
- $('.accept-mr-form :input').disable();
- });
-
- $(document)
- .off('click', '.accept_merge_request')
- .on('click', '.accept_merge_request', () => {
- $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
- });
-
- $(document)
- .off('click', '.merge_when_build_succeeds')
- .on('click', '.merge_when_build_succeeds', () => {
- $('#merge_when_build_succeeds').val('1');
- });
-
- $(document)
- .off('click', '.js-merge-dropdown a')
- .on('click', '.js-merge-dropdown a', (e) => {
- e.preventDefault();
- $(e.target).closest('form').submit();
- });
- if ($('.rebase-in-progress').length) {
- merge_request_widget.rebaseInProgress();
- } else if ($('.rebase-mr-form').length) {
- $(document)
- .off('ajax:send', '.rebase-mr-form')
- .on('ajax:send', '.rebase-mr-form', () => {
- $('.rebase-mr-form :input').disable();
- });
-
- $(document)
- .off('click', '.js-rebase-button')
- .on('click', '.js-rebase-button', () => {
- $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
- });
- } else {
- setTimeout(() => merge_request_widget.getMergeStatus(), 200);
- }
- });
-})();
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7fbaeec7882..38c673e8907 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -78,7 +78,6 @@
} else {
$(element).find('.assignee-icon').empty();
}
- return $(element).effect('highlight');
};
function Milestone() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 8df1c8e7f94..40e977df693 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -19,7 +19,7 @@
}
$els.each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
@@ -29,6 +29,7 @@
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
+ showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
issuableId = $dropdown.data('issuable-id');
@@ -39,7 +40,7 @@
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
}
@@ -71,6 +72,13 @@
title: 'Upcoming'
});
}
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: 'Started'
+ });
+ }
if (extraOptions.length) {
extraOptions.push('divider');
}
@@ -124,18 +132,12 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
- !$dropdown.closest('.add-issues-modal').length) {
- boardsStore = gl.issueBoards.BoardsStore.state.filters;
- } else if ($dropdown.closest('.add-issues-modal').length) {
+ if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
- if (!$dropdown.closest('.add-issues-modal').length) {
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- }
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
@@ -157,7 +159,7 @@
}
$dropdown.trigger('loading.gl.dropdown');
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
@@ -169,7 +171,7 @@
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
@@ -181,8 +183,7 @@
$selectbox.hide();
$value.css('display', '');
if (data.milestone != null) {
- data.milestone.namespace = _this.currentProject.namespace;
- data.milestone.path = _this.currentProject.path;
+ data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
new file mode 100644
index 00000000000..9c58c465001
--- /dev/null
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -0,0 +1,110 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+/**
+ * In each pipelines table we have a mini pipeline graph for each pipeline.
+ *
+ * When we click in a pipeline stage, we need to make an API call to get the
+ * builds list to render in a dropdown.
+ *
+ * The container should be the table element.
+ *
+ * The stage icon clicked needs to have the following HTML structure:
+ * <div class="dropdown">
+ * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
+ * <div class="js-builds-dropdown-container dropdown-menu"></div>
+ * </div>
+ */
+
+export default class MiniPipelineGraph {
+ constructor(opts = {}) {
+ this.container = opts.container || '';
+ this.dropdownListSelector = '.js-builds-dropdown-container';
+ this.getBuildsList = this.getBuildsList.bind(this);
+ }
+
+ /**
+ * Adds the event listener when the dropdown is opened.
+ * All dropdown events are fired at the .dropdown-menu's parent element.
+ */
+ bindEvents() {
+ $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
+ }
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(document).on(
+ 'click',
+ `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`,
+ (e) => {
+ e.stopPropagation();
+ },
+ );
+ }
+
+ /**
+ * For the clicked stage, renders the given data in the dropdown list.
+ *
+ * @param {HTMLElement} stageContainer
+ * @param {Object} data
+ */
+ renderBuildsList(stageContainer, data) {
+ const dropdownContainer = stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ );
+
+ dropdownContainer.innerHTML = data;
+ }
+
+ /**
+ * For the clicked stage, gets the list of builds.
+ *
+ * All dropdown events have a relatedTarget property,
+ * whose value is the toggling anchor element.
+ *
+ * @param {Object} e bootstrap dropdown event
+ * @return {Promise}
+ */
+ getBuildsList(e) {
+ const button = e.relatedTarget;
+ const endpoint = button.dataset.stageEndpoint;
+
+ return $.ajax({
+ dataType: 'json',
+ type: 'GET',
+ url: endpoint,
+ beforeSend: () => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+ },
+ success: (data) => {
+ this.toggleLoading(button);
+ this.renderBuildsList(button, data.html);
+ this.stopDropdownClickPropagation();
+ },
+ error: () => {
+ this.toggleLoading(button);
+ new Flash('An error occurred while fetching the builds.', 'alert');
+ },
+ });
+ }
+
+ /**
+ * Toggles the visibility of the loading icon.
+ *
+ * @param {HTMLElement} stageContainer
+ * @return {type}
+ */
+ toggleLoading(stageContainer) {
+ stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+ ).classList.toggle('hidden');
+ }
+}
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
deleted file mode 100644
index 2145e531331..00000000000
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ /dev/null
@@ -1,95 +0,0 @@
-/* eslint-disable no-new */
-/* global Flash */
-
-/**
- * In each pipelines table we have a mini pipeline graph for each pipeline.
- *
- * When we click in a pipeline stage, we need to make an API call to get the
- * builds list to render in a dropdown.
- *
- * The container should be the table element.
- *
- * The stage icon clicked needs to have the following HTML structure:
- * <div class="dropdown">
- * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
- * <div class="js-builds-dropdown-container dropdown-menu"></div>
- * </div>
- */
-(() => {
- class MiniPipelineGraph {
- constructor(opts = {}) {
- this.container = opts.container || '';
- this.dropdownListSelector = '.js-builds-dropdown-container';
- this.getBuildsList = this.getBuildsList.bind(this);
- }
-
- /**
- * Adds the event listener when the dropdown is opened.
- * All dropdown events are fired at the .dropdown-menu's parent element.
- */
- bindEvents() {
- $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
- }
-
- /**
- * For the clicked stage, renders the given data in the dropdown list.
- *
- * @param {HTMLElement} stageContainer
- * @param {Object} data
- */
- renderBuildsList(stageContainer, data) {
- const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
- );
-
- dropdownContainer.innerHTML = data;
- }
-
- /**
- * For the clicked stage, gets the list of builds.
- *
- * All dropdown events have a relatedTarget property,
- * whose value is the toggling anchor element.
- *
- * @param {Object} e bootstrap dropdown event
- * @return {Promise}
- */
- getBuildsList(e) {
- const button = e.relatedTarget;
- const endpoint = button.dataset.stageEndpoint;
-
- return $.ajax({
- dataType: 'json',
- type: 'GET',
- url: endpoint,
- beforeSend: () => {
- this.renderBuildsList(button, '');
- this.toggleLoading(button);
- },
- success: (data) => {
- this.toggleLoading(button);
- this.renderBuildsList(button, data.html);
- },
- error: () => {
- this.toggleLoading(button);
- new Flash('An error occurred while fetching the builds.', 'alert');
- },
- });
- }
-
- /**
- * Toggles the visibility of the loading icon.
- *
- * @param {HTMLElement} stageContainer
- * @return {type}
- */
- toggleLoading(stageContainer) {
- stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-loading`,
- ).classList.toggle('hidden');
- }
- }
-
- window.gl = window.gl || {};
- window.gl.MiniPipelineGraph = MiniPipelineGraph;
-})();
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 00000000000..b3ce9310417
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+ document.removeEventListener('DOMContentLoaded', onLoad, false);
+ return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
new file mode 100644
index 00000000000..fcffc11a2df
--- /dev/null
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -0,0 +1,335 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+import d3 from 'd3';
+import statusCodes from '~/lib/utils/http_status';
+import '../lib/utils/common_utils';
+import '../flash';
+
+const prometheusGraphsContainer = '.prometheus-graph';
+const metricsEndpoint = 'metrics.json';
+const timeFormat = d3.time.format('%H:%M');
+const dayFormat = d3.time.format('%b %e, %a');
+const bisectDate = d3.bisector(d => d.time).left;
+const extraAddedWidthParent = 100;
+
+class PrometheusGraph {
+
+ constructor() {
+ this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
+ this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
+ const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
+ extraAddedWidthParent;
+ this.originalWidth = parentContainerWidth;
+ this.originalHeight = 400;
+ this.width = parentContainerWidth - this.margin.left - this.margin.right;
+ this.height = 400 - this.margin.top - this.margin.bottom;
+ this.backOffRequestCounter = 0;
+ this.configureGraph();
+ this.init();
+ }
+
+ createGraph() {
+ Object.keys(this.data).forEach((key) => {
+ const value = this.data[key];
+ if (value.length > 0) {
+ this.plotValues(value, key);
+ }
+ });
+ }
+
+ init() {
+ this.getData().then((metricsResponse) => {
+ if (Object.keys(metricsResponse).length === 0) {
+ new Flash('Empty metrics', 'alert');
+ } else {
+ this.transformData(metricsResponse);
+ this.createGraph();
+ }
+ });
+ }
+
+ plotValues(valuesToPlot, key) {
+ const x = d3.time.scale()
+ .range([0, this.width]);
+
+ const y = d3.scale.linear()
+ .range([this.height, 0]);
+
+ const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
+
+ const graphSpecifics = this.graphSpecificProperties[key];
+
+ const chart = d3.select(prometheusGraphContainer)
+ .attr('width', this.width + this.margin.left + this.margin.right)
+ .attr('height', this.height + this.margin.bottom + this.margin.top)
+ .append('g')
+ .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
+
+ const axisLabelContainer = d3.select(prometheusGraphContainer)
+ .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
+ .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
+ .append('g')
+ .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
+
+ x.domain(d3.extent(valuesToPlot, d => d.time));
+ y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
+
+ const xAxis = d3.svg.axis()
+ .scale(x)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .orient('bottom');
+
+ const yAxis = d3.svg.axis()
+ .scale(y)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .tickSize(-this.width)
+ .orient('left');
+
+ this.createAxisLabelContainers(axisLabelContainer, key);
+
+ chart.append('g')
+ .attr('class', 'x-axis')
+ .attr('transform', `translate(0,${this.height})`)
+ .call(xAxis);
+
+ chart.append('g')
+ .attr('class', 'y-axis')
+ .call(yAxis);
+
+ const area = d3.svg.area()
+ .x(d => x(d.time))
+ .y0(this.height)
+ .y1(d => y(d.value))
+ .interpolate('linear');
+
+ const line = d3.svg.line()
+ .x(d => x(d.time))
+ .y(d => y(d.value));
+
+ chart.append('path')
+ .datum(valuesToPlot)
+ .attr('d', area)
+ .attr('class', 'metric-area')
+ .attr('fill', graphSpecifics.area_fill_color);
+
+ chart.append('path')
+ .datum(valuesToPlot)
+ .attr('class', 'metric-line')
+ .attr('stroke', graphSpecifics.line_color)
+ .attr('fill', 'none')
+ .attr('stroke-width', this.commonGraphProperties.area_stroke_width)
+ .attr('d', line);
+
+ // Overlay area for the mouseover events
+ chart.append('rect')
+ .attr('class', 'prometheus-graph-overlay')
+ .attr('width', this.width)
+ .attr('height', this.height)
+ .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
+ }
+
+ // The legends from the metric
+ createAxisLabelContainers(axisLabelContainer, key) {
+ const graphSpecifics = this.graphSpecificProperties[key];
+
+ axisLabelContainer.append('line')
+ .attr('class', 'label-x-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 0,
+ y1: this.originalHeight - this.marginLabelContainer.top,
+ x2: this.originalWidth - this.margin.right,
+ y2: this.originalHeight - this.marginLabelContainer.top,
+ });
+
+ axisLabelContainer.append('line')
+ .attr('class', 'label-y-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 0,
+ y1: 0,
+ x2: 0,
+ y2: this.originalHeight - this.marginLabelContainer.top,
+ });
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('text-anchor', 'middle')
+ .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
+ .text(graphSpecifics.graph_legend_title);
+
+ axisLabelContainer.append('rect')
+ .attr('class', 'rect-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
+ .attr('width', 30)
+ .attr('height', 80);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - this.marginLabelContainer.top)
+ .attr('dy', '.35em')
+ .text('Time');
+
+ // Legends
+
+ // Metric Usage
+ axisLabelContainer.append('rect')
+ .attr('x', this.originalWidth - 170)
+ .attr('y', (this.originalHeight / 2) - 80)
+ .style('fill', graphSpecifics.area_fill_color)
+ .attr('width', 20)
+ .attr('height', 35);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 65)
+ .text(graphSpecifics.graph_legend_title);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'text-metric-usage')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 50);
+ }
+
+ handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
+ const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
+ const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
+ const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
+ const d0 = valuesToPlot[timeValueIndex - 1];
+ const d1 = valuesToPlot[timeValueIndex];
+ const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
+ const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
+ const currentTimeCoordinate = x(currentData.time);
+ const graphSpecifics = this.graphSpecificProperties[key];
+ // Remove the current selectors
+ d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
+
+ chart.append('line')
+ .attr('class', 'selected-metric-line')
+ .attr({
+ x1: currentTimeCoordinate,
+ y1: y(0),
+ x2: currentTimeCoordinate,
+ y2: maxValueMetric,
+ });
+
+ chart.append('circle')
+ .attr('class', 'circle-metric')
+ .attr('fill', graphSpecifics.line_color)
+ .attr('cx', currentTimeCoordinate)
+ .attr('cy', y(currentData.value))
+ .attr('r', this.commonGraphProperties.circle_radius_metric);
+
+ // The little box with text
+ const rectTextMetric = chart.append('g')
+ .attr('class', 'rect-text-metric')
+ .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
+
+ rectTextMetric.append('rect')
+ .attr('class', 'rect-metric')
+ .attr('x', currentTimeCoordinate + 10)
+ .attr('y', maxValueMetric)
+ .attr('width', this.commonGraphProperties.rect_text_width)
+ .attr('height', this.commonGraphProperties.rect_text_height);
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric')
+ .attr('x', currentTimeCoordinate + 35)
+ .attr('y', maxValueMetric + 35)
+ .text(timeFormat(currentData.time));
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric-date')
+ .attr('x', currentTimeCoordinate + 15)
+ .attr('y', maxValueMetric + 15)
+ .text(dayFormat(currentData.time));
+
+ // Update the text
+ d3.select(`${prometheusGraphContainer} .text-metric-usage`)
+ .text(currentData.value.substring(0, 8));
+ }
+
+ configureGraph() {
+ this.graphSpecificProperties = {
+ cpu_values: {
+ area_fill_color: '#edf3fc',
+ line_color: '#5b99f7',
+ graph_legend_title: 'CPU Usage (Cores)',
+ },
+ memory_values: {
+ area_fill_color: '#fca326',
+ line_color: '#fc6d26',
+ graph_legend_title: 'Memory Usage (MB)',
+ },
+ };
+
+ this.commonGraphProperties = {
+ area_stroke_width: 2,
+ median_total_characters: 8,
+ circle_radius_metric: 5,
+ rect_text_width: 90,
+ rect_text_height: 40,
+ axis_no_ticks: 3,
+ };
+ }
+
+ getData() {
+ const maxNumberOfRequests = 3;
+ return gl.utils.backOff((next, stop) => {
+ $.ajax({
+ url: metricsEndpoint,
+ dataType: 'json',
+ })
+ .done((data, statusText, resp) => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ if (this.backOffRequestCounter < maxNumberOfRequests) {
+ next();
+ } else {
+ stop({
+ status: resp.status,
+ metrics: data,
+ });
+ }
+ } else {
+ stop({
+ status: resp.status,
+ metrics: data,
+ });
+ }
+ }).fail(stop);
+ })
+ .then((resp) => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ return {};
+ }
+ return resp.metrics;
+ })
+ .catch(() => new Flash('An error occurred while fetching metrics.', 'alert'));
+ }
+
+ transformData(metricsResponse) {
+ const metricTypes = {};
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const metricValues = (metricsResponse.metrics[key])[0];
+ metricTypes[key] = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
+ });
+ this.data = metricTypes;
+ }
+}
+
+export default PrometheusGraph;
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 43dc9838977..5aad3908eb6 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,424 +1,347 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
-/* global Raphael */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+import Raphael from './raphael';
- this.BranchGraph = (function() {
- function BranchGraph(element1, options1) {
- this.element = element1;
- this.options = options1;
- this.scrollTop = bind(this.scrollTop, this);
- this.scrollBottom = bind(this.scrollBottom, this);
- this.scrollRight = bind(this.scrollRight, this);
- this.scrollLeft = bind(this.scrollLeft, this);
- this.scrollUp = bind(this.scrollUp, this);
- this.scrollDown = bind(this.scrollDown, this);
- this.preparedCommits = {};
- this.mtime = 0;
- this.mspace = 0;
- this.parents = {};
- this.colors = ["#000"];
- this.offsetX = 150;
- this.offsetY = 20;
- this.unitTime = 30;
- this.unitSpace = 10;
- this.prev_start = -1;
- this.load();
- }
-
- BranchGraph.prototype.load = function() {
- return $.ajax({
- url: this.options.url,
- method: "get",
- dataType: "json",
- success: $.proxy(function(data) {
- $(".loading", this.element).hide();
- this.prepareData(data.days, data.commits);
- return this.buildGraph();
- }, this)
- });
- };
+export default (function() {
+ function BranchGraph(element1, options1) {
+ this.element = element1;
+ this.options = options1;
+ this.scrollTop = this.scrollTop.bind(this);
+ this.scrollBottom = this.scrollBottom.bind(this);
+ this.scrollRight = this.scrollRight.bind(this);
+ this.scrollLeft = this.scrollLeft.bind(this);
+ this.scrollUp = this.scrollUp.bind(this);
+ this.scrollDown = this.scrollDown.bind(this);
+ this.preparedCommits = {};
+ this.mtime = 0;
+ this.mspace = 0;
+ this.parents = {};
+ this.colors = ["#000"];
+ this.offsetX = 150;
+ this.offsetY = 20;
+ this.unitTime = 30;
+ this.unitSpace = 10;
+ this.prev_start = -1;
+ this.load();
+ }
- BranchGraph.prototype.prepareData = function(days, commits) {
- var c, ch, cw, j, len, ref;
- this.days = days;
- this.commits = commits;
- this.collectParents();
- this.graphHeight = $(this.element).height();
- this.graphWidth = $(this.element).width();
- ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
- cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
- this.r = Raphael(this.element.get(0), cw, ch);
- this.top = this.r.set();
- this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
- ref = this.commits;
- for (j = 0, len = ref.length; j < len; j += 1) {
- c = ref[j];
- if (c.id in this.parents) {
- c.isParent = true;
- }
- this.preparedCommits[c.id] = c;
- this.markCommit(c);
- }
- return this.collectColors();
- };
-
- BranchGraph.prototype.collectParents = function() {
- var c, j, len, p, ref, results;
- ref = this.commits;
- results = [];
- for (j = 0, len = ref.length; j < len; j += 1) {
- c = ref[j];
- this.mtime = Math.max(this.mtime, c.time);
- this.mspace = Math.max(this.mspace, c.space);
- results.push((function() {
- var l, len1, ref1, results1;
- ref1 = c.parents;
- results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l += 1) {
- p = ref1[l];
- this.parents[p[0]] = true;
- results1.push(this.mspace = Math.max(this.mspace, p[1]));
- }
- return results1;
- }).call(this));
- }
- return results;
- };
+ BranchGraph.prototype.load = function() {
+ return $.ajax({
+ url: this.options.url,
+ method: "get",
+ dataType: "json",
+ success: $.proxy(function(data) {
+ $(".loading", this.element).hide();
+ this.prepareData(data.days, data.commits);
+ return this.buildGraph();
+ }, this)
+ });
+ };
- BranchGraph.prototype.collectColors = function() {
- var k, results;
- k = 0;
- results = [];
- while (k < this.mspace) {
- this.colors.push(Raphael.getColor(.8));
- // Skipping a few colors in the spectrum to get more contrast between colors
- Raphael.getColor();
- Raphael.getColor();
- results.push(k += 1);
+ BranchGraph.prototype.prepareData = function(days, commits) {
+ var c, ch, cw, j, len, ref;
+ this.days = days;
+ this.commits = commits;
+ this.collectParents();
+ this.graphHeight = $(this.element).height();
+ this.graphWidth = $(this.element).width();
+ ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
+ cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
+ this.r = Raphael(this.element.get(0), cw, ch);
+ this.top = this.r.set();
+ this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
+ ref = this.commits;
+ for (j = 0, len = ref.length; j < len; j += 1) {
+ c = ref[j];
+ if (c.id in this.parents) {
+ c.isParent = true;
}
- return results;
- };
+ this.preparedCommits[c.id] = c;
+ this.markCommit(c);
+ }
+ return this.collectColors();
+ };
- BranchGraph.prototype.buildGraph = function() {
- var cuday, cumonth, day, j, len, mm, r, ref;
- r = this.r;
- cuday = 0;
- cumonth = "";
- r.rect(0, 0, 40, this.barHeight).attr({
- fill: "#222"
- });
- r.rect(40, 0, 30, this.barHeight).attr({
- fill: "#444"
- });
- ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
- day = ref[mm];
- if (cuday !== day[0] || cumonth !== day[1]) {
- // Dates
- r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
- font: "12px Monaco, monospace",
- fill: "#BBB"
- });
- cuday = day[0];
- }
- if (cumonth !== day[1]) {
- // Months
- r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
- font: "12px Monaco, monospace",
- fill: "#EEE"
- });
- cumonth = day[1];
+ BranchGraph.prototype.collectParents = function() {
+ var c, j, len, p, ref, results;
+ ref = this.commits;
+ results = [];
+ for (j = 0, len = ref.length; j < len; j += 1) {
+ c = ref[j];
+ this.mtime = Math.max(this.mtime, c.time);
+ this.mspace = Math.max(this.mspace, c.space);
+ results.push((function() {
+ var l, len1, ref1, results1;
+ ref1 = c.parents;
+ results1 = [];
+ for (l = 0, len1 = ref1.length; l < len1; l += 1) {
+ p = ref1[l];
+ this.parents[p[0]] = true;
+ results1.push(this.mspace = Math.max(this.mspace, p[1]));
}
- }
- this.renderPartialGraph();
- return this.bindEvents();
- };
+ return results1;
+ }).call(this));
+ }
+ return results;
+ };
- BranchGraph.prototype.renderPartialGraph = function() {
- var commit, end, i, isGraphEdge, start, x, y;
- start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
- if (start < 0) {
- isGraphEdge = true;
- start = 0;
+ BranchGraph.prototype.collectColors = function() {
+ var k, results;
+ k = 0;
+ results = [];
+ while (k < this.mspace) {
+ this.colors.push(Raphael.getColor(.8));
+ // Skipping a few colors in the spectrum to get more contrast between colors
+ Raphael.getColor();
+ Raphael.getColor();
+ results.push(k += 1);
+ }
+ return results;
+ };
+
+ BranchGraph.prototype.buildGraph = function() {
+ var cuday, cumonth, day, j, len, mm, r, ref;
+ r = this.r;
+ cuday = 0;
+ cumonth = "";
+ r.rect(0, 0, 40, this.barHeight).attr({
+ fill: "#222"
+ });
+ r.rect(40, 0, 30, this.barHeight).attr({
+ fill: "#444"
+ });
+ ref = this.days;
+ for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
+ day = ref[mm];
+ if (cuday !== day[0] || cumonth !== day[1]) {
+ // Dates
+ r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
+ font: "12px Monaco, monospace",
+ fill: "#BBB"
+ });
+ cuday = day[0];
}
- end = start + 40;
- if (this.commits.length < end) {
- isGraphEdge = true;
- end = this.commits.length;
+ if (cumonth !== day[1]) {
+ // Months
+ r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
+ font: "12px Monaco, monospace",
+ fill: "#EEE"
+ });
+ cumonth = day[1];
}
- if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
- i = start;
- this.prev_start = start;
- while (i < end) {
- commit = this.commits[i];
- i += 1;
- if (commit.hasDrawn !== true) {
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
- this.drawDot(x, y, commit);
- this.drawLines(x, y, commit);
- this.appendLabel(x, y, commit);
- this.appendAnchor(x, y, commit);
- commit.hasDrawn = true;
- }
+ }
+ this.renderPartialGraph();
+ return this.bindEvents();
+ };
+
+ BranchGraph.prototype.renderPartialGraph = function() {
+ var commit, end, i, isGraphEdge, start, x, y;
+ start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
+ if (start < 0) {
+ isGraphEdge = true;
+ start = 0;
+ }
+ end = start + 40;
+ if (this.commits.length < end) {
+ isGraphEdge = true;
+ end = this.commits.length;
+ }
+ if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
+ i = start;
+ this.prev_start = start;
+ while (i < end) {
+ commit = this.commits[i];
+ i += 1;
+ if (commit.hasDrawn !== true) {
+ x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ y = this.offsetY + this.unitTime * commit.time;
+ this.drawDot(x, y, commit);
+ this.drawLines(x, y, commit);
+ this.appendLabel(x, y, commit);
+ this.appendAnchor(x, y, commit);
+ commit.hasDrawn = true;
}
- return this.top.toFront();
}
- };
-
- BranchGraph.prototype.bindEvents = function() {
- var element;
- element = this.element;
- return $(element).scroll((function(_this) {
- return function(event) {
- return _this.renderPartialGraph();
- };
- })(this));
- };
-
- BranchGraph.prototype.scrollDown = function() {
- this.element.scrollTop(this.element.scrollTop() + 50);
- return this.renderPartialGraph();
- };
-
- BranchGraph.prototype.scrollUp = function() {
- this.element.scrollTop(this.element.scrollTop() - 50);
- return this.renderPartialGraph();
- };
-
- BranchGraph.prototype.scrollLeft = function() {
- this.element.scrollLeft(this.element.scrollLeft() - 50);
- return this.renderPartialGraph();
- };
-
- BranchGraph.prototype.scrollRight = function() {
- this.element.scrollLeft(this.element.scrollLeft() + 50);
- return this.renderPartialGraph();
- };
-
- BranchGraph.prototype.scrollBottom = function() {
- return this.element.scrollTop(this.element.find('svg').height());
- };
+ return this.top.toFront();
+ }
+ };
- BranchGraph.prototype.scrollTop = function() {
- return this.element.scrollTop(0);
- };
+ BranchGraph.prototype.bindEvents = function() {
+ var element;
+ element = this.element;
+ return $(element).scroll((function(_this) {
+ return function(event) {
+ return _this.renderPartialGraph();
+ };
+ })(this));
+ };
- BranchGraph.prototype.appendLabel = function(x, y, commit) {
- var label, r, rect, shortrefs, text, textbox, triangle;
- if (!commit.refs) {
- return;
- }
- r = this.r;
- shortrefs = commit.refs;
- // Truncate if longer than 15 chars
- if (shortrefs.length > 17) {
- shortrefs = shortrefs.substr(0, 15) + "…";
- }
- text = r.text(x + 4, y, shortrefs).attr({
- "text-anchor": "start",
- font: "10px Monaco, monospace",
- fill: "#FFF",
- title: commit.refs
- });
- textbox = text.getBBox();
- // Create rectangle based on the size of the textbox
- rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
- fill: "#000",
- "fill-opacity": .5,
- stroke: "none"
- });
- triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({
- fill: "#000",
- "fill-opacity": .5,
- stroke: "none"
- });
- label = r.set(rect, text);
- label.transform(["t", -rect.getBBox().width - 15, 0]);
- // Set text to front
- return text.toFront();
- };
+ BranchGraph.prototype.scrollDown = function() {
+ this.element.scrollTop(this.element.scrollTop() + 50);
+ return this.renderPartialGraph();
+ };
- BranchGraph.prototype.appendAnchor = function(x, y, commit) {
- var anchor, options, r, top;
- r = this.r;
- top = this.top;
- options = this.options;
- anchor = r.circle(x, y, 10).attr({
- fill: "#000",
- opacity: 0,
- cursor: "pointer"
- }).click(function() {
- return window.open(options.commit_url.replace("%s", commit.id), "_blank");
- }).hover(function() {
- this.tooltip = r.commitTooltip(x + 5, y, commit);
- return top.push(this.tooltip.insertBefore(this));
- }, function() {
- return this.tooltip && this.tooltip.remove() && delete this.tooltip;
- });
- return top.push(anchor);
- };
+ BranchGraph.prototype.scrollUp = function() {
+ this.element.scrollTop(this.element.scrollTop() - 50);
+ return this.renderPartialGraph();
+ };
- BranchGraph.prototype.drawDot = function(x, y, commit) {
- var avatar_box_x, avatar_box_y, r;
- r = this.r;
- r.circle(x, y, 3).attr({
- fill: this.colors[commit.space],
- stroke: "none"
- });
- avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
- avatar_box_y = y - 10;
- r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
- stroke: this.colors[commit.space],
- "stroke-width": 2
- });
- r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20);
- return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({
- "text-anchor": "start",
- font: "14px Monaco, monospace"
- });
- };
+ BranchGraph.prototype.scrollLeft = function() {
+ this.element.scrollLeft(this.element.scrollLeft() - 50);
+ return this.renderPartialGraph();
+ };
- BranchGraph.prototype.drawLines = function(x, y, commit) {
- var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
- r = this.r;
- ref = commit.parents;
- results = [];
- for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
- parent = ref[i];
- parentCommit = this.preparedCommits[parent[0]];
- parentY = this.offsetY + this.unitTime * parentCommit.time;
- parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
- parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
- // Set line color
- if (parentCommit.space <= commit.space) {
- color = this.colors[commit.space];
- } else {
- color = this.colors[parentCommit.space];
- }
- // Build line shape
- if (parent[1] === commit.space) {
- offset = [0, 5];
- arrow = "l-2,5,4,0,-2,-5,0,5";
- } else if (parent[1] < commit.space) {
- offset = [3, 3];
- arrow = "l5,0,-2,4,-3,-4,4,2";
- } else {
- offset = [-3, 3];
- arrow = "l-5,0,2,4,3,-4,-4,2";
- }
- // Start point
- route = ["M", x + offset[0], y + offset[1]];
- // Add arrow if not first parent
- if (i > 0) {
- route.push(arrow);
- }
- // Circumvent if overlap
- if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
- route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
- }
- // End point
- route.push("L", parentX1, parentY);
- results.push(r.path(route).attr({
- stroke: color,
- "stroke-width": 2
- }));
- }
- return results;
- };
+ BranchGraph.prototype.scrollRight = function() {
+ this.element.scrollLeft(this.element.scrollLeft() + 50);
+ return this.renderPartialGraph();
+ };
- BranchGraph.prototype.markCommit = function(commit) {
- var r, x, y;
- if (commit.id === this.options.commit_id) {
- r = this.r;
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
- r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
- fill: "#000",
- "fill-opacity": .5,
- stroke: "none"
- });
- // Displayed in the center
- return this.element.scrollTop(y - this.graphHeight / 2);
- }
- };
+ BranchGraph.prototype.scrollBottom = function() {
+ return this.element.scrollTop(this.element.find('svg').height());
+ };
- return BranchGraph;
- })();
+ BranchGraph.prototype.scrollTop = function() {
+ return this.element.scrollTop(0);
+ };
- Raphael.prototype.commitTooltip = function(x, y, commit) {
- var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip;
- boxWidth = 300;
- boxHeight = 200;
- icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
- nameText = this.text(x + 25, y + 10, commit.author.name);
- idText = this.text(x, y + 35, commit.id);
- messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
- textSet = this.set(icon, nameText, idText, messageText).attr({
+ BranchGraph.prototype.appendLabel = function(x, y, commit) {
+ var label, r, rect, shortrefs, text, textbox, triangle;
+ if (!commit.refs) {
+ return;
+ }
+ r = this.r;
+ shortrefs = commit.refs;
+ // Truncate if longer than 15 chars
+ if (shortrefs.length > 17) {
+ shortrefs = shortrefs.substr(0, 15) + "…";
+ }
+ text = r.text(x + 4, y, shortrefs).attr({
"text-anchor": "start",
- font: "12px Monaco, monospace"
- });
- nameText.attr({
- font: "14px Arial",
- "font-weight": "bold"
+ font: "10px Monaco, monospace",
+ fill: "#FFF",
+ title: commit.refs
});
- idText.attr({
- fill: "#AAA"
+ textbox = text.getBBox();
+ // Create rectangle based on the size of the textbox
+ rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
});
- messageText.node.style["white-space"] = "pre";
- this.textWrap(messageText, boxWidth - 50);
- rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
- fill: "#FFF",
- stroke: "#000",
- "stroke-linecap": "round",
- "stroke-width": 2
+ triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
});
- tooltip = this.set(rect, textSet);
- rect.attr({
- height: tooltip.getBBox().height + 10,
- width: tooltip.getBBox().width + 10
+ label = r.set(rect, text);
+ label.transform(["t", -rect.getBBox().width - 15, 0]);
+ // Set text to front
+ return text.toFront();
+ };
+
+ BranchGraph.prototype.appendAnchor = function(x, y, commit) {
+ var anchor, options, r, top;
+ r = this.r;
+ top = this.top;
+ options = this.options;
+ anchor = r.circle(x, y, 10).attr({
+ fill: "#000",
+ opacity: 0,
+ cursor: "pointer"
+ }).click(function() {
+ return window.open(options.commit_url.replace("%s", commit.id), "_blank");
+ }).hover(function() {
+ this.tooltip = r.commitTooltip(x + 5, y, commit);
+ return top.push(this.tooltip.insertBefore(this));
+ }, function() {
+ return this.tooltip && this.tooltip.remove() && delete this.tooltip;
});
- tooltip.transform(["t", 20, 20]);
- return tooltip;
+ return top.push(anchor);
};
- Raphael.prototype.textWrap = function(t, width) {
- var abc, b, content, h, j, len, letterWidth, s, word, words, x;
- content = t.attr("text");
- abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
- t.attr({
- text: abc
+ BranchGraph.prototype.drawDot = function(x, y, commit) {
+ var avatar_box_x, avatar_box_y, r;
+ r = this.r;
+ r.circle(x, y, 3).attr({
+ fill: this.colors[commit.space],
+ stroke: "none"
});
- letterWidth = t.getBBox().width / abc.length;
- t.attr({
- text: content
+ avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+ avatar_box_y = y - 10;
+ r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
+ stroke: this.colors[commit.space],
+ "stroke-width": 2
});
- words = content.split(" ");
- x = 0;
- s = [];
- for (j = 0, len = words.length; j < len; j += 1) {
- word = words[j];
- if (x + (word.length * letterWidth) > width) {
- s.push("\n");
- x = 0;
+ r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20);
+ return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({
+ "text-anchor": "start",
+ font: "14px Monaco, monospace"
+ });
+ };
+
+ BranchGraph.prototype.drawLines = function(x, y, commit) {
+ var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
+ r = this.r;
+ ref = commit.parents;
+ results = [];
+ for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
+ parent = ref[i];
+ parentCommit = this.preparedCommits[parent[0]];
+ parentY = this.offsetY + this.unitTime * parentCommit.time;
+ parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
+ parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+ // Set line color
+ if (parentCommit.space <= commit.space) {
+ color = this.colors[commit.space];
+ } else {
+ color = this.colors[parentCommit.space];
}
- if (word === "\n") {
- s.push("\n");
- x = 0;
+ // Build line shape
+ if (parent[1] === commit.space) {
+ offset = [0, 5];
+ arrow = "l-2,5,4,0,-2,-5,0,5";
+ } else if (parent[1] < commit.space) {
+ offset = [3, 3];
+ arrow = "l5,0,-2,4,-3,-4,4,2";
} else {
- s.push(word + " ");
- x += word.length * letterWidth;
+ offset = [-3, 3];
+ arrow = "l-5,0,2,4,3,-4,-4,2";
+ }
+ // Start point
+ route = ["M", x + offset[0], y + offset[1]];
+ // Add arrow if not first parent
+ if (i > 0) {
+ route.push(arrow);
+ }
+ // Circumvent if overlap
+ if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
+ route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
}
+ // End point
+ route.push("L", parentX1, parentY);
+ results.push(r.path(route).attr({
+ stroke: color,
+ "stroke-width": 2
+ }));
+ }
+ return results;
+ };
+
+ BranchGraph.prototype.markCommit = function(commit) {
+ var r, x, y;
+ if (commit.id === this.options.commit_id) {
+ r = this.r;
+ x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ y = this.offsetY + this.unitTime * commit.time;
+ r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
+ });
+ // Displayed in the center
+ return this.element.scrollTop(y - this.graphHeight / 2);
}
- t.attr({
- text: s.join("").trim()
- });
- b = t.getBBox();
- h = Math.abs(b.y2) + 1;
- return t.attr({
- y: h
- });
};
-}).call(window);
+
+ return BranchGraph;
+})();
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 8e7027b44e7..a3fd22aff2a 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,20 +1,19 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
-/* global BranchGraph */
-(function() {
- this.Network = (function() {
- function Network(opts) {
- var vph;
- $("#filter_ref").click(function() {
- return $(this).closest('form').submit();
- });
- this.branch_graph = new BranchGraph($(".network-graph"), opts);
- vph = $(window).height() - 250;
- $('.network-graph').css({
- 'height': vph + 'px'
- });
- }
+import BranchGraph from './branch_graph';
- return Network;
- })();
-}).call(window);
+export default (function() {
+ function Network(opts) {
+ var vph;
+ $("#filter_ref").click(function() {
+ return $(this).closest('form').submit();
+ });
+ this.branch_graph = new BranchGraph($(".network-graph"), opts);
+ vph = $(window).height() - 250;
+ $('.network-graph').css({
+ 'height': vph + 'px'
+ });
+ }
+
+ return Network;
+})();
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index aae509caa79..8aae2ad201c 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,22 +1,17 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
-/* global Network */
/* global ShortcutsNetwork */
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
+import Network from './network';
-(function() {
- $(function() {
- if (!$(".network-graph").length) return;
+$(function() {
+ if (!$(".network-graph").length) return;
- var network_graph;
- network_graph = new Network({
- url: $(".network-graph").attr('data-url'),
- commit_url: $(".network-graph").attr('data-commit-url'),
- ref: $(".network-graph").attr('data-ref'),
- commit_id: $(".network-graph").attr('data-commit-id')
- });
- return new ShortcutsNetwork(network_graph.branch_graph);
+ var network_graph;
+ network_graph = new Network({
+ url: $(".network-graph").attr('data-url'),
+ commit_url: $(".network-graph").attr('data-commit-url'),
+ ref: $(".network-graph").attr('data-ref'),
+ commit_id: $(".network-graph").attr('data-commit-id')
});
-}).call(window);
+ return new ShortcutsNetwork(network_graph.branch_graph);
+});
diff --git a/app/assets/javascripts/network/raphael.js b/app/assets/javascripts/network/raphael.js
new file mode 100644
index 00000000000..09dcf716148
--- /dev/null
+++ b/app/assets/javascripts/network/raphael.js
@@ -0,0 +1,74 @@
+import Raphael from 'raphael/raphael';
+
+Raphael.prototype.commitTooltip = function commitTooltip(x, y, commit) {
+ const boxWidth = 300;
+ const icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
+ const nameText = this.text(x + 25, y + 10, commit.author.name);
+ const idText = this.text(x, y + 35, commit.id);
+ const messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, ' \n '));
+ const textSet = this.set(icon, nameText, idText, messageText).attr({
+ 'text-anchor': 'start',
+ font: '12px Monaco, monospace',
+ });
+ nameText.attr({
+ font: '14px Arial',
+ 'font-weight': 'bold',
+ });
+ idText.attr({
+ fill: '#AAA',
+ });
+ messageText.node.style['white-space'] = 'pre';
+ this.textWrap(messageText, boxWidth - 50);
+ const rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
+ fill: '#FFF',
+ stroke: '#000',
+ 'stroke-linecap': 'round',
+ 'stroke-width': 2,
+ });
+ const tooltip = this.set(rect, textSet);
+ rect.attr({
+ height: tooltip.getBBox().height + 10,
+ width: tooltip.getBBox().width + 10,
+ });
+ tooltip.transform(['t', 20, 20]);
+ return tooltip;
+};
+
+Raphael.prototype.textWrap = function testWrap(t, width) {
+ const content = t.attr('text');
+ const abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ t.attr({
+ text: abc,
+ });
+ const letterWidth = t.getBBox().width / abc.length;
+ t.attr({
+ text: content,
+ });
+ const words = content.split(' ');
+ let x = 0;
+ const s = [];
+ for (let j = 0, len = words.length; j < len; j += 1) {
+ const word = words[j];
+ if (x + (word.length * letterWidth) > width) {
+ s.push('\n');
+ x = 0;
+ }
+ if (word === '\n') {
+ s.push('\n');
+ x = 0;
+ } else {
+ s.push(`${word} `);
+ x += word.length * letterWidth;
+ }
+ }
+ t.attr({
+ text: s.join('').trim(),
+ });
+ const b = t.getBBox();
+ const h = Math.abs(b.y2) + 1;
+ return t.attr({
+ y: h,
+ });
+};
+
+export default Raphael;
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index cb24f212c66..5828f460a23 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -20,15 +20,35 @@
};
NewBranchForm.prototype.init = function() {
- if (this.name.val().length > 0) {
+ if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
};
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
- return this.ref.autocomplete({
- source: availableRefs,
- minLength: 1
+ var $branchSelect = $('.js-branch-select');
+
+ $branchSelect.glDropdown({
+ data: availableRefs,
+ filterable: true,
+ filterByText: true,
+ remote: false,
+ fieldName: $branchSelect.data('field-name'),
+ selectable: true,
+ isSelectable: function(branch, $el) {
+ return !$el.hasClass('is-active');
+ },
+ text: function(branch) {
+ return branch;
+ },
+ id: function(branch) {
+ return branch;
+ },
+ toggleLabel: function(branch) {
+ if (branch) {
+ return branch;
+ }
+ }
});
};
@@ -61,7 +81,7 @@
var errorMessage, errors, formatter, unique, validator;
this.branchNameError.empty();
unique = function(values, value) {
- if (indexOf.call(values, value) < 0) {
+ if (indexOf.call(values, value) === -1) {
values.push(value);
}
return values;
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 747f693726e..ad36f08840d 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -3,19 +3,23 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
- function NewCommitForm(form) {
+ function NewCommitForm(form, targetBranchName = 'target_branch') {
+ this.form = form;
+ this.targetBranchName = targetBranchName;
this.renderDestination = bind(this.renderDestination, this);
- this.newBranch = form.find('.js-target-branch');
+ this.targetBranchDropdown = form.find('button.js-target-branch');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.targetBranchDropdown.on('change.branch', this.renderDestination);
this.renderDestination();
- this.newBranch.keyup(this.renderDestination);
}
NewCommitForm.prototype.renderDestination = function() {
var different;
- different = this.newBranch.val() !== this.originalBranch.val();
+ var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`);
+
+ different = targetBranch.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 03504255bda..47cc34e7a20 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,12 +1,14 @@
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global Autosave */
+/* global Cookies */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
+window.Cookies = require('js-cookie');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
@@ -42,7 +44,6 @@ require('./task_list');
this.notes_url = notes_url;
this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at;
- this.view = view;
this.noteable_url = document.URL;
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000;
@@ -57,6 +58,7 @@ require('./task_list');
selector: '.notes'
});
this.collapseLongCommitList();
+ this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab
if (gl.utils.getPagePath(1) === 'merge_requests') {
@@ -65,6 +67,10 @@ require('./task_list');
}
}
+ Notes.prototype.setViewType = function(view) {
+ this.view = Cookies.get('diff_view') || view;
+ };
+
Notes.prototype.addBinding = function() {
// add note to UI after creation
$(document).on("ajax:success", ".js-main-target-form", this.addNote);
@@ -198,7 +204,7 @@ require('./task_list');
this.refreshing = true;
return $.ajax({
url: this.notes_url,
- data: "last_fetched_at=" + this.last_fetched_at,
+ headers: { "X-Last-Fetched-At": this.last_fetched_at },
dataType: "json",
success: (function(_this) {
return function(data) {
@@ -246,12 +252,21 @@ require('./task_list');
};
Notes.prototype.handleCreateChanges = function(note) {
+ var votesBlock;
if (typeof note === 'undefined') {
return;
}
- if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
- $.get(mrRefreshWidgetUrl);
+ if (note.commands_changes) {
+ if ('merge' in note.commands_changes) {
+ $.get(mrRefreshWidgetUrl);
+ }
+
+ if ('emoji_award' in note.commands_changes) {
+ votesBlock = $('.js-awards-block').eq(0);
+ gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
+ return gl.awardsHandler.scrollToAwards();
+ }
}
};
@@ -262,26 +277,16 @@ require('./task_list');
*/
Notes.prototype.renderNote = function(note) {
- var $notesList, votesBlock;
+ var $notesList;
if (!note.valid) {
- if (note.award) {
- new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
- }
- else {
- if (note.errors.commands_only) {
- new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
- this.refresh();
- }
+ if (note.errors.commands_only) {
+ new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+ this.refresh();
}
return;
}
- if (note.award) {
- votesBlock = $('.js-awards-block').eq(0);
- gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
- return gl.awardsHandler.scrollToAwards();
- // render note if it not present in loaded list
- // or skip if rendered
- } else if (this.isNewNote(note)) {
+
+ if (this.isNewNote(note)) {
this.note_ids.push(note.id);
$notesList = $('ul.main-notes-list');
$notesList.append(note.html).syntaxHighlight();
@@ -303,7 +308,7 @@ require('./task_list');
};
Notes.prototype.isParallelView = function() {
- return this.view === 'parallel';
+ return Cookies.get('diff_view') === 'parallel';
};
/*
@@ -313,7 +318,7 @@ require('./task_list');
*/
Notes.prototype.renderDiscussionNote = function(note) {
- var discussionContainer, form, note_html, row;
+ var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) {
return;
}
@@ -323,6 +328,8 @@ require('./task_list');
form = $("#new-discussion-note-form-" + note.original_discussion_id);
}
row = form.closest("tr");
+ lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
+ diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
note_html = $(note.html);
note_html.renderGFM();
// is this the first note of discussion?
@@ -331,10 +338,26 @@ require('./task_list');
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
- // insert the note and the reply button after the temp row
- row.after(note.diff_discussion_html);
- // remove the note (will be added again below)
- row.next().find(".note").remove();
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
+ // insert the note and the reply button after the temp row
+ row.after(note.diff_discussion_html);
+
+ // remove the note (will be added again below)
+ row.next().find(".note").remove();
+ } else {
+ // Merge new discussion HTML in
+ var $discussion = $(note.diff_discussion_html);
+ var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
+ var contentContainerClass = '.' + $notes.closest('.notes_content')
+ .attr('class')
+ .split(' ')
+ .join('.');
+
+ // remove the note (will be added again below)
+ $notes.find('.note').remove();
+
+ row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
+ }
// Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
// Add note to 'Changes' page discussions
@@ -348,14 +371,40 @@ require('./task_list');
discussionContainer.append(note_html);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
gl.diffNotesCompileComponents();
+ this.renderDiscussionAvatar(diffAvatarContainer, note);
}
gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1);
};
+ Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
+ return $(changesDiscussionContainer).closest('.notes_holder')
+ .prevAll('.line_holder')
+ .first()
+ .get(0);
+ };
+
+ Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
+ var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
+ var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
+
+ if (!avatarHolder.length) {
+ avatarHolder = document.createElement('diff-note-avatars');
+ avatarHolder.setAttribute('discussion-id', note.discussion_id);
+
+ diffAvatarContainer.append(avatarHolder);
+
+ gl.diffNotesCompileComponents();
+ }
+
+ if (commentButton.length) {
+ commentButton.remove();
+ }
+ };
+
/*
Called in response the main target form has been successfully submitted.
@@ -593,9 +642,14 @@ require('./task_list');
*/
Notes.prototype.removeNote = function(e) {
- var noteId;
- noteId = $(e.currentTarget).closest(".note").attr("id");
- $(".note[id='" + noteId + "']").each((function(_this) {
+ var noteElId, noteId, dataNoteId, $note, lineHolder;
+ $note = $(e.currentTarget).closest('.note');
+ noteElId = $note.attr('id');
+ noteId = $note.attr('data-note-id');
+ lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
+ .closest('.notes_holder')
+ .prev('.line_holder');
+ $(".note[id='" + noteElId + "']").each((function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
// where $("#noteId") would return only one.
@@ -605,17 +659,26 @@ require('./task_list');
notes = note.closest(".notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- if (gl.diffNoteApps[noteId]) {
- gl.diffNoteApps[noteId].$destroy();
+ if (gl.diffNoteApps[noteElId]) {
+ gl.diffNoteApps[noteElId].$destroy();
}
}
+ note.remove();
+
// check if this is the last note for this line
- if (notes.find(".note").length === 1) {
+ if (notes.find(".note").length === 0) {
+ var notesTr = notes.closest("tr");
+
// "Discussions" tab
notes.closest(".timeline-entry").remove();
- // "Changes" tab / commit view
- notes.closest("tr").remove();
+
+ if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
+ // "Changes" tab / commit view
+ notesTr.remove();
+ } else {
+ notes.closest('.content').empty();
+ }
}
return note.remove();
};
@@ -708,15 +771,16 @@ require('./task_list');
*/
Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
e.preventDefault();
- $link = $(e.currentTarget);
+ $link = $(e.currentTarget || e.target);
row = $link.closest("tr");
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
addForm = false;
notesContentSelector = ".notes_content";
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
@@ -724,7 +788,9 @@ require('./task_list');
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
notesContentSelector += " .content";
- if (hasNotes) {
+ notesContent = nextRow.find(notesContentSelector);
+
+ if (hasNotes && !isDiffCommentAvatar) {
nextRow.show();
notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) {
@@ -741,13 +807,21 @@ require('./task_list');
}
}
}
- } else {
+ } else if (!isDiffCommentAvatar) {
// add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
addForm = true;
+ } else {
+ nextRow.show();
+ notesContent.toggle(!notesContent.is(':visible'));
+
+ if (!nextRow.find('.content:not(:empty)').is(':visible')) {
+ nextRow.hide();
+ }
}
+
if (addForm) {
newForm = this.formClone.clone();
newForm.appendTo(notesContent);
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
new file mode 100644
index 00000000000..5f6bc902cf8
--- /dev/null
+++ b/app/assets/javascripts/pager.js
@@ -0,0 +1,77 @@
+require('~/lib/utils/common_utils');
+require('~/lib/utils/url_utility');
+
+(() => {
+ const ENDLESS_SCROLL_BOTTOM_PX = 400;
+ const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
+
+ const Pager = {
+ init(limit = 0, preload = false, disable = false, callback = $.noop) {
+ this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
+ this.limit = limit;
+ this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
+ this.disable = disable;
+ this.callback = callback;
+ this.loading = $('.loading').first();
+ if (preload) {
+ this.offset = 0;
+ this.getOld();
+ }
+ this.initLoadMore();
+ },
+
+ getOld() {
+ this.loading.show();
+ $.ajax({
+ type: 'GET',
+ url: this.url,
+ data: `limit=${this.limit}&offset=${this.offset}`,
+ dataType: 'json',
+ error: () => this.loading.hide(),
+ success: (data) => {
+ this.append(data.count, data.html);
+ this.callback();
+
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ },
+ });
+ },
+
+ append(count, html) {
+ $('.content_list').append(html);
+ if (count > 0) {
+ this.offset += count;
+ } else {
+ this.disable = true;
+ }
+ },
+
+ isScrollable() {
+ const $w = $(window);
+ return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
+ },
+
+ initLoadMore() {
+ $(document).unbind('scroll');
+ $(document).endlessScroll({
+ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
+ fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
+ fireOnce: true,
+ ceaseFire: () => this.disable === true,
+ callback: () => {
+ if (!this.loading.is(':visible')) {
+ this.loading.show();
+ this.getOld();
+ }
+ },
+ });
+ },
+ };
+
+ window.Pager = Pager;
+})();
diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6
deleted file mode 100644
index e35cf6d295e..00000000000
--- a/app/assets/javascripts/pager.js.es6
+++ /dev/null
@@ -1,73 +0,0 @@
-(() => {
- const ENDLESS_SCROLL_BOTTOM_PX = 400;
- const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
-
- const Pager = {
- init(limit = 0, preload = false, disable = false, callback = $.noop) {
- this.limit = limit;
- this.offset = this.limit;
- this.disable = disable;
- this.callback = callback;
- this.loading = $('.loading').first();
- if (preload) {
- this.offset = 0;
- this.getOld();
- }
- this.initLoadMore();
- },
-
- getOld() {
- this.loading.show();
- $.ajax({
- type: 'GET',
- url: $('.content_list').data('href') || window.location.href,
- data: `limit=${this.limit}&offset=${this.offset}`,
- dataType: 'json',
- error: () => this.loading.hide(),
- success: (data) => {
- this.append(data.count, data.html);
- this.callback();
-
- // keep loading until we've filled the viewport height
- if (!this.disable && !this.isScrollable()) {
- this.getOld();
- } else {
- this.loading.hide();
- }
- },
- });
- },
-
- append(count, html) {
- $('.content_list').append(html);
- if (count > 0) {
- this.offset += count;
- } else {
- this.disable = true;
- }
- },
-
- isScrollable() {
- const $w = $(window);
- return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
- },
-
- initLoadMore() {
- $(document).unbind('scroll');
- $(document).endlessScroll({
- bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
- fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
- fireOnce: true,
- ceaseFire: () => this.disable === true,
- callback: () => {
- if (!this.loading.is(':visible')) {
- this.loading.show();
- this.getOld();
- }
- },
- });
- },
- };
-
- window.Pager = Pager;
-})();
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js
index 9203abefbbc..9203abefbbc 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
new file mode 100644
index 00000000000..cf1566eeb87
--- /dev/null
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -0,0 +1,173 @@
+/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
+
+import 'vendor/cropper';
+
+((global) => {
+ // Matches everything but the file name
+ const FILENAMEREGEX = /^.*[\\\/]/;
+
+ class GitLabCrop {
+ constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
+ exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
+ this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
+ this.onModalHide = this.onModalHide.bind(this);
+ this.onModalShow = this.onModalShow.bind(this);
+ this.onPickImageClick = this.onPickImageClick.bind(this);
+ this.fileInput = $(input);
+ this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
+ this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`);
+ this.exportWidth = exportWidth;
+ this.exportHeight = exportHeight;
+ this.cropBoxWidth = cropBoxWidth;
+ this.cropBoxHeight = cropBoxHeight;
+ this.form = this.fileInput.parents('form');
+ this.filename = filename;
+ this.previewImage = previewImage;
+ this.modalCrop = modalCrop;
+ this.pickImageEl = pickImageEl;
+ this.uploadImageBtn = uploadImageBtn;
+ this.modalCropImg = modalCropImg;
+ this.filename = this.getElement(filename);
+ this.previewImage = this.getElement(previewImage);
+ this.pickImageEl = this.getElement(pickImageEl);
+ this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
+ this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
+ this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
+ this.cropActionsBtn = this.modalCrop.find('[data-method]');
+ this.bindEvents();
+ }
+
+ getElement(selector) {
+ return $(selector, this.form);
+ }
+
+ bindEvents() {
+ var _this;
+ _this = this;
+ this.fileInput.on('change', function(e) {
+ return _this.onFileInputChange(e, this);
+ });
+ this.pickImageEl.on('click', this.onPickImageClick);
+ this.modalCrop.on('shown.bs.modal', this.onModalShow);
+ this.modalCrop.on('hidden.bs.modal', this.onModalHide);
+ this.uploadImageBtn.on('click', this.onUploadImageBtnClick);
+ this.cropActionsBtn.on('click', function(e) {
+ var btn;
+ btn = this;
+ return _this.onActionBtnClick(btn);
+ });
+ return this.croppedImageBlob = null;
+ }
+
+ onPickImageClick() {
+ return this.fileInput.trigger('click');
+ }
+
+ onModalShow() {
+ var _this;
+ _this = this;
+ return this.modalCropImg.cropper({
+ viewMode: 1,
+ center: false,
+ aspectRatio: 1,
+ modal: true,
+ scalable: false,
+ rotatable: false,
+ zoomable: true,
+ dragMode: 'move',
+ guides: false,
+ zoomOnTouch: false,
+ zoomOnWheel: false,
+ cropBoxMovable: false,
+ cropBoxResizable: false,
+ toggleDragModeOnDblclick: false,
+ built: function() {
+ var $image, container, cropBoxHeight, cropBoxWidth;
+ $image = $(this);
+ container = $image.cropper('getContainerData');
+ cropBoxWidth = _this.cropBoxWidth;
+ cropBoxHeight = _this.cropBoxHeight;
+ return $image.cropper('setCropBoxData', {
+ width: cropBoxWidth,
+ height: cropBoxHeight,
+ left: (container.width - cropBoxWidth) / 2,
+ top: (container.height - cropBoxHeight) / 2
+ });
+ }
+ });
+ }
+
+ onModalHide() {
+ return this.modalCropImg.attr('src', '').cropper('destroy');
+ }
+
+ onUploadImageBtnClick(e) {
+ e.preventDefault();
+ this.setBlob();
+ this.setPreview();
+ this.modalCrop.modal('hide');
+ return this.fileInput.val('');
+ }
+
+ onActionBtnClick(btn) {
+ var data, result;
+ data = $(btn).data();
+ if (this.modalCropImg.data('cropper') && data.method) {
+ return result = this.modalCropImg.cropper(data.method, data.option);
+ }
+ }
+
+ onFileInputChange(e, input) {
+ return this.readFile(input);
+ }
+
+ readFile(input) {
+ var _this, reader;
+ _this = this;
+ reader = new FileReader;
+ reader.onload = () => {
+ _this.modalCropImg.attr('src', reader.result);
+ return _this.modalCrop.modal('show');
+ };
+ return reader.readAsDataURL(input.files[0]);
+ }
+
+ dataURLtoBlob(dataURL) {
+ var array, binary, i, k, len, v;
+ binary = atob(dataURL.split(',')[1]);
+ array = [];
+ for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
+ v = binary[k];
+ array.push(binary.charCodeAt(k));
+ }
+ return new Blob([new Uint8Array(array)], {
+ type: 'image/png'
+ });
+ }
+
+ setPreview() {
+ var filename;
+ this.previewImage.attr('src', this.dataURL);
+ filename = this.fileInput.val().replace(FILENAMEREGEX, '');
+ return this.filename.text(filename);
+ }
+
+ setBlob() {
+ this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
+ width: 200,
+ height: 200
+ }).toDataURL('image/png');
+ return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
+ }
+
+ getBlob() {
+ return this.croppedImageBlob;
+ }
+ }
+
+ $.fn.glCrop = function(opts) {
+ return this.each(function() {
+ return $(this).data('glcrop', new GitLabCrop(this, opts));
+ });
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
deleted file mode 100644
index 42e9847af91..00000000000
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ /dev/null
@@ -1,171 +0,0 @@
-/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
-
-((global) => {
- // Matches everything but the file name
- const FILENAMEREGEX = /^.*[\\\/]/;
-
- class GitLabCrop {
- constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
- exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
- this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
- this.onModalHide = this.onModalHide.bind(this);
- this.onModalShow = this.onModalShow.bind(this);
- this.onPickImageClick = this.onPickImageClick.bind(this);
- this.fileInput = $(input);
- this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
- this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
- this.exportWidth = exportWidth;
- this.exportHeight = exportHeight;
- this.cropBoxWidth = cropBoxWidth;
- this.cropBoxHeight = cropBoxHeight;
- this.form = this.fileInput.parents('form');
- this.filename = filename;
- this.previewImage = previewImage;
- this.modalCrop = modalCrop;
- this.pickImageEl = pickImageEl;
- this.uploadImageBtn = uploadImageBtn;
- this.modalCropImg = modalCropImg;
- this.filename = this.getElement(filename);
- this.previewImage = this.getElement(previewImage);
- this.pickImageEl = this.getElement(pickImageEl);
- this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
- this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
- this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
- this.cropActionsBtn = this.modalCrop.find('[data-method]');
- this.bindEvents();
- }
-
- getElement(selector) {
- return $(selector, this.form);
- }
-
- bindEvents() {
- var _this;
- _this = this;
- this.fileInput.on('change', function(e) {
- return _this.onFileInputChange(e, this);
- });
- this.pickImageEl.on('click', this.onPickImageClick);
- this.modalCrop.on('shown.bs.modal', this.onModalShow);
- this.modalCrop.on('hidden.bs.modal', this.onModalHide);
- this.uploadImageBtn.on('click', this.onUploadImageBtnClick);
- this.cropActionsBtn.on('click', function(e) {
- var btn;
- btn = this;
- return _this.onActionBtnClick(btn);
- });
- return this.croppedImageBlob = null;
- }
-
- onPickImageClick() {
- return this.fileInput.trigger('click');
- }
-
- onModalShow() {
- var _this;
- _this = this;
- return this.modalCropImg.cropper({
- viewMode: 1,
- center: false,
- aspectRatio: 1,
- modal: true,
- scalable: false,
- rotatable: false,
- zoomable: true,
- dragMode: 'move',
- guides: false,
- zoomOnTouch: false,
- zoomOnWheel: false,
- cropBoxMovable: false,
- cropBoxResizable: false,
- toggleDragModeOnDblclick: false,
- built: function() {
- var $image, container, cropBoxHeight, cropBoxWidth;
- $image = $(this);
- container = $image.cropper('getContainerData');
- cropBoxWidth = _this.cropBoxWidth;
- cropBoxHeight = _this.cropBoxHeight;
- return $image.cropper('setCropBoxData', {
- width: cropBoxWidth,
- height: cropBoxHeight,
- left: (container.width - cropBoxWidth) / 2,
- top: (container.height - cropBoxHeight) / 2
- });
- }
- });
- }
-
- onModalHide() {
- return this.modalCropImg.attr('src', '').cropper('destroy');
- }
-
- onUploadImageBtnClick(e) {
- e.preventDefault();
- this.setBlob();
- this.setPreview();
- this.modalCrop.modal('hide');
- return this.fileInput.val('');
- }
-
- onActionBtnClick(btn) {
- var data, result;
- data = $(btn).data();
- if (this.modalCropImg.data('cropper') && data.method) {
- return result = this.modalCropImg.cropper(data.method, data.option);
- }
- }
-
- onFileInputChange(e, input) {
- return this.readFile(input);
- }
-
- readFile(input) {
- var _this, reader;
- _this = this;
- reader = new FileReader;
- reader.onload = () => {
- _this.modalCropImg.attr('src', reader.result);
- return _this.modalCrop.modal('show');
- };
- return reader.readAsDataURL(input.files[0]);
- }
-
- dataURLtoBlob(dataURL) {
- var array, binary, i, k, len, v;
- binary = atob(dataURL.split(',')[1]);
- array = [];
- for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
- v = binary[k];
- array.push(binary.charCodeAt(k));
- }
- return new Blob([new Uint8Array(array)], {
- type: 'image/png'
- });
- }
-
- setPreview() {
- var filename;
- this.previewImage.attr('src', this.dataURL);
- filename = this.fileInput.val().replace(FILENAMEREGEX, '');
- return this.filename.text(filename);
- }
-
- setBlob() {
- this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
- width: 200,
- height: 200
- }).toDataURL('image/png');
- return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
- }
-
- getBlob() {
- return this.croppedImageBlob;
- }
- }
-
- $.fn.glCrop = function(opts) {
- return this.each(function() {
- return $(this).data('glcrop', new GitLabCrop(this, opts));
- });
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
new file mode 100644
index 00000000000..c38bc762675
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js
@@ -0,0 +1,99 @@
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
+/* global Flash */
+
+((global) => {
+ class Profile {
+ constructor({ form } = {}) {
+ this.onSubmitForm = this.onSubmitForm.bind(this);
+ this.form = form || $('.edit-user');
+ this.bindEvents();
+ this.initAvatarGlCrop();
+ }
+
+ initAvatarGlCrop() {
+ const cropOpts = {
+ filename: '.js-avatar-filename',
+ previewImage: '.avatar-image .avatar',
+ modalCrop: '.modal-profile-crop',
+ pickImageEl: '.js-choose-user-avatar-button',
+ uploadImageBtn: '.js-upload-user-avatar',
+ modalCropImg: '.modal-profile-crop-image'
+ };
+ this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ }
+
+ bindEvents() {
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('#user_notification_email').on('change', this.submitForm);
+ $('.update-username').on('ajax:before', this.beforeUpdateUsername);
+ $('.update-username').on('ajax:complete', this.afterUpdateUsername);
+ $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
+ this.form.on('submit', this.onSubmitForm);
+ }
+
+ submitForm() {
+ return $(this).parents('form').submit();
+ }
+
+ onSubmitForm(e) {
+ e.preventDefault();
+ return this.saveForm();
+ }
+
+ beforeUpdateUsername() {
+ $('.loading-username', this).removeClass('hidden');
+ }
+
+ afterUpdateUsername() {
+ $('.loading-username', this).addClass('hidden');
+ $('button[type=submit]', this).enable();
+ }
+
+ onUpdateNotifs(e, data) {
+ return data.saved ?
+ new Flash("Notification settings saved", "notice") :
+ new Flash("Failed to save new settings", "alert");
+ }
+
+ saveForm() {
+ const self = this;
+ const formData = new FormData(this.form[0]);
+ const avatarBlob = this.avatarGlCrop.getBlob();
+
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
+ }
+
+ return $.ajax({
+ url: this.form.attr('action'),
+ type: this.form.attr('method'),
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ success: response => new Flash(response.message, 'notice'),
+ error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
+ complete: () => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ return self.form.find(':input[disabled]').enable();
+ }
+ });
+ }
+ }
+
+ $(function() {
+ $(document).on('input.ssh_key', '#key_key', function() {
+ const $title = $('#key_title');
+ const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+
+ // Extract the SSH Key title from its comment
+ if (comment && comment.length > 1) {
+ return $title.val(comment[1]).change();
+ }
+ });
+ if (global.utils.getPagePath() === 'profiles') {
+ return new Profile();
+ }
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
deleted file mode 100644
index 81374296522..00000000000
--- a/app/assets/javascripts/profile/profile.js.es6
+++ /dev/null
@@ -1,99 +0,0 @@
-/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
-/* global Flash */
-
-((global) => {
- class Profile {
- constructor({ form } = {}) {
- this.onSubmitForm = this.onSubmitForm.bind(this);
- this.form = form || $('.edit-user');
- this.bindEvents();
- this.initAvatarGlCrop();
- }
-
- initAvatarGlCrop() {
- const cropOpts = {
- filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
- modalCrop: '.modal-profile-crop',
- pickImageEl: '.js-choose-user-avatar-button',
- uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image'
- };
- this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
- }
-
- bindEvents() {
- $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('#user_notification_email').on('change', this.submitForm);
- $('#user_notified_of_own_activity').on('change', this.submitForm);
- $('.update-username').on('ajax:before', this.beforeUpdateUsername);
- $('.update-username').on('ajax:complete', this.afterUpdateUsername);
- $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
- this.form.on('submit', this.onSubmitForm);
- }
-
- submitForm() {
- return $(this).parents('form').submit();
- }
-
- onSubmitForm(e) {
- e.preventDefault();
- return this.saveForm();
- }
-
- beforeUpdateUsername() {
- $('.loading-username', this).removeClass('hidden');
- }
-
- afterUpdateUsername() {
- $('.loading-username', this).addClass('hidden');
- $('button[type=submit]', this).enable();
- }
-
- onUpdateNotifs(e, data) {
- return data.saved ?
- new Flash("Notification settings saved", "notice") :
- new Flash("Failed to save new settings", "alert");
- }
-
- saveForm() {
- const self = this;
- const formData = new FormData(this.form[0]);
- const avatarBlob = this.avatarGlCrop.getBlob();
-
- if (avatarBlob != null) {
- formData.append('user[avatar]', avatarBlob, 'avatar.png');
- }
-
- return $.ajax({
- url: this.form.attr('action'),
- type: this.form.attr('method'),
- data: formData,
- dataType: "json",
- processData: false,
- contentType: false,
- success: response => new Flash(response.message, 'notice'),
- error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
- complete: () => {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- return self.form.find(':input[disabled]').enable();
- }
- });
- }
- }
-
- $(function() {
- $(document).on('focusout.ssh_key', '#key_key', function() {
- const $title = $('#key_title');
- const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
- if (comment && comment.length > 1 && $title.val() === '') {
- return $title.val(comment[1]).change();
- }
- // Extract the SSH Key title from its comment
- });
- if (global.utils.getPagePath() === 'profiles') {
- return new Profile();
- }
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index d7f3c9fd37e..15d32825583 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,3 +1,2 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
+require('./gl_crop');
+require('./profile');
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 7c03c8b72d4..db7ceaa2421 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -116,7 +116,7 @@
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
var action = $form.attr('action');
- var divider = action.indexOf('?') < 0 ? '?' : '&';
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js
index 0a811627600..0a811627600 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index f80e765ce30..3c1c1e7dceb 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -35,7 +35,7 @@
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
- return Api.projects(term, orderBy, projectsCallback);
+ return Api.projects(term, { order_by: orderBy }, projectsCallback);
}
},
url: function(project) {
@@ -84,7 +84,7 @@
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
- return Api.projects(query.term, _this.orderBy, projectsCallback);
+ return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback);
}
};
})(this),
diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js
index 4ee2e49306d..4ee2e49306d 100644
--- a/app/assets/javascripts/project_variables.js.es6
+++ b/app/assets/javascripts/project_variables.js
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index acdf9b7eb5a..c67d59d2be5 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,50 +1,18 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */
+import FilterableList from './filterable_list';
-(function() {
- window.ProjectsList = {
- init: function() {
- $(".projects-list-filter").off('keyup');
- this.initSearch();
- return this.initPagination();
- },
- initSearch: function() {
- var debounceFilter, projectsListFilter;
- projectsListFilter = $('.projects-list-filter');
- debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
- return projectsListFilter.on('keyup', function(e) {
- if (projectsListFilter.val() !== '') {
- return debounceFilter();
- }
- });
- },
- filterResults: function() {
- var form, project_filter_url, search;
- $('.projects-list-holder').fadeTo(250, 0.5);
- form = null;
- form = $("form#project-filter-form");
- search = $(".projects-list-filter").val();
- project_filter_url = form.attr('action') + '?' + form.serialize();
- return $.ajax({
- type: "GET",
- url: form.attr('action'),
- data: form.serialize(),
- complete: function() {
- return $('.projects-list-holder').fadeTo(250, 1);
- },
- success: function(data) {
- $('.projects-list-holder').replaceWith(data.html);
- return history.replaceState({
- page: project_filter_url
- // Change url so if user reload a page - search results are saved
- }, document.title, project_filter_url);
- },
- dataType: "json"
- });
- },
- initPagination: function() {
- return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) {
- return $('.projects-list-holder').replaceWith(data.html);
- });
+/**
+ * Makes search request for projects when user types a value in the search input.
+ * Updates the html content of the page with the received one.
+ */
+export default class ProjectsList {
+ constructor() {
+ const form = document.querySelector('form#project-filter-form');
+ const filter = document.querySelector('.js-projects-list-filter');
+ const holder = document.querySelector('.js-projects-list-holder');
+
+ if (form && filter && holder) {
+ const list = new FilterableList(form, filter, holder);
+ list.initSearch();
}
- };
-}).call(window);
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
index e7fff57ff45..e7fff57ff45 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 57ea2f52814..57ea2f52814 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index 5cf28aa7a73..5cf28aa7a73 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
new file mode 100644
index 00000000000..6ef59e94384
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -0,0 +1,69 @@
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
+/* global Flash */
+
+(global => {
+ global.gl = global.gl || {};
+
+ gl.ProtectedBranchEdit = class {
+ constructor(options) {
+ this.$wrap = options.$wrap;
+ this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+ this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+
+ this.buildDropdowns();
+ }
+
+ buildDropdowns() {
+ // Allowed to merge dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToMergeDropdown,
+ data: gon.merge_access_levels,
+ onSelect: this.onSelect.bind(this)
+ });
+
+ // Allowed to push dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToPushDropdown,
+ data: gon.push_access_levels,
+ onSelect: this.onSelect.bind(this)
+ });
+ }
+
+ onSelect() {
+ const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
+ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+
+ // Do not update if one dropdown has not selected any option
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+
+ this.$allowedToMergeDropdown.disable();
+ this.$allowedToPushDropdown.disable();
+
+ $.ajax({
+ type: 'POST',
+ url: this.$wrap.data('url'),
+ dataType: 'json',
+ data: {
+ _method: 'PATCH',
+ protected_branch: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
+ access_level: $allowedToMergeInput.val()
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
+ access_level: $allowedToPushInput.val()
+ }]
+ }
+ },
+ error() {
+ $.scrollTo(0);
+ new Flash('Failed to update branch!');
+ }
+ }).always(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+ });
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
deleted file mode 100644
index 149e511451e..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ /dev/null
@@ -1,66 +0,0 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
-/* global Flash */
-
-(global => {
- global.gl = global.gl || {};
-
- gl.ProtectedBranchEdit = class {
- constructor(options) {
- this.$wrap = options.$wrap;
- this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
- this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
-
- this.buildDropdowns();
- }
-
- buildDropdowns() {
- // Allowed to merge dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: this.$allowedToMergeDropdown,
- data: gon.merge_access_levels,
- onSelect: this.onSelect.bind(this)
- });
-
- // Allowed to push dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: this.$allowedToPushDropdown,
- data: gon.push_access_levels,
- onSelect: this.onSelect.bind(this)
- });
- }
-
- onSelect() {
- const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
- const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
-
- // Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
-
- $.ajax({
- type: 'POST',
- url: this.$wrap.data('url'),
- dataType: 'json',
- data: {
- _method: 'PATCH',
- protected_branch: {
- merge_access_levels_attributes: [{
- id: this.$allowedToMergeDropdown.data('access-level-id'),
- access_level: $allowedToMergeInput.val()
- }],
- push_access_levels_attributes: [{
- id: this.$allowedToPushDropdown.data('access-level-id'),
- access_level: $allowedToPushInput.val()
- }]
- }
- },
- success: () => {
- this.$wrap.effect('highlight');
- },
- error() {
- $.scrollTo(0);
- new Flash('Failed to update branch!');
- }
- });
- }
- };
-})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
index 336fa6c57a7..336fa6c57a7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index ffb66caf5f4..849c1e31623 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,3 +1,5 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
+require('./protected_branch_access_dropdown');
+require('./protected_branch_create');
+require('./protected_branch_dropdown');
+require('./protected_branch_edit');
+require('./protected_branch_edit_list');
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index e66418beeab..15f5963353a 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -47,7 +47,7 @@
fields: ['name']
},
data: function(term, callback) {
- return Api.projects(term, 'id', function(data) {
+ return Api.projects(term, { order_by: 'id' }, function(data) {
data.unshift({
name_with_namespace: 'Any'
});
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js
index 6fd5345a0a6..6fd5345a0a6 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js
diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js
index bfe90aef71e..bfe90aef71e 100644
--- a/app/assets/javascripts/shortcuts_blob.js.es6
+++ b/app/assets/javascripts/shortcuts_blob.js
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 542cd586df0..09a58cad2b2 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -32,7 +32,7 @@ require('./shortcuts');
return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
});
Mousetrap.bind('g g', function() {
- return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs');
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts');
});
Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js
index d811d1cd53a..d811d1cd53a 100644
--- a/app/assets/javascripts/signin_tabs_memoizer.js.es6
+++ b/app/assets/javascripts/signin_tabs_memoizer.js
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js
index d1bdc353be2..d1bdc353be2 100644
--- a/app/assets/javascripts/smart_interval.js.es6
+++ b/app/assets/javascripts/smart_interval.js
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 89822246bb8..a98403f4cf2 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,10 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
-
(function() {
$(function() {
var editor = ace.edit("editor");
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js
index 2128007113f..2128007113f 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js
index d8191605128..d8191605128 100644
--- a/app/assets/javascripts/subbable_resource.js.es6
+++ b/app/assets/javascripts/subbable_resource.js
diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js
index 62d1604fe9e..62d1604fe9e 100644
--- a/app/assets/javascripts/subscription.js.es6
+++ b/app/assets/javascripts/subscription.js
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index dfe24d1fb33..b1402c0a880 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,3 +1,4 @@
+/* global Flash */
require('vendor/task_list');
class TaskList {
@@ -6,6 +7,16 @@ class TaskList {
this.dataType = options.dataType;
this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {});
+ this.onError = function showFlash(response) {
+ let errorMessages = '';
+
+ if (response.responseJSON) {
+ errorMessages = response.responseJSON.errors.join(' ');
+ }
+
+ return new Flash(errorMessages || 'Update failed', 'alert');
+ };
+
this.init();
}
@@ -32,6 +43,7 @@ class TaskList {
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData,
success: this.onSuccess,
+ error: this.onError,
});
}
}
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js
index e9e9aafd71a..e9e9aafd71a 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js
index 97f6d37364d..97f6d37364d 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js
diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js
index 6b9422b1816..6b9422b1816 100644
--- a/app/assets/javascripts/terminal/terminal.js.es6
+++ b/app/assets/javascripts/terminal/terminal.js
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js
index 13cf3a10a38..13cf3a10a38 100644
--- a/app/assets/javascripts/terminal/terminal_bundle.js.es6
+++ b/app/assets/javascripts/terminal/terminal_bundle.js
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index 7dba5840c8a..d48f2404fa5 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -43,7 +43,14 @@
return event;
}
- function getTraget(target) {
+ function isLast(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return children.length - 1 === target.index;
+ }
+
+ function getTarget(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children;
@@ -75,12 +82,22 @@
function simulateDrag(options, callback) {
options.to.el = options.to.el || options.from.el;
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
+ var fromEl = getTarget(options.from);
+ var toEl = getTarget(options.to);
+ var firstEl = getTarget({
+ el: options.to.el,
+ index: 'first'
+ });
+ var lastEl = getTarget({
+ el: options.to.el,
+ index: 'last'
+ });
var scrollable = options.scrollable;
var fromRect = getRect(fromEl);
var toRect = getRect(toEl);
+ var firstRect = getRect(firstEl);
+ var lastRect = getRect(lastEl);
var startTime = new Date().getTime();
var duration = options.duration || 1000;
@@ -88,6 +105,12 @@
options.ontap && options.ontap();
window.SIMULATE_DRAG_ACTIVE = 1;
+ if (options.to.index === 0) {
+ toRect.cy = firstRect.y;
+ } else if (isLast(options.to)) {
+ toRect.cy = lastRect.y + lastRect.h + 50;
+ }
+
var dragInterval = setInterval(function loop() {
var progress = (new Date().getTime() - startTime) / duration;
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
new file mode 100644
index 00000000000..8be58023c84
--- /dev/null
+++ b/app/assets/javascripts/todos.js
@@ -0,0 +1,163 @@
+/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
+/* global UsersSelect */
+
+class Todos {
+ constructor() {
+ this.initFilters();
+ this.bindEvents();
+ this.todo_ids = [];
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ unbindEvents() {
+ $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
+ $('.todo').off('click', this.goToTodoUrl);
+ }
+
+ bindEvents() {
+ this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
+ this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this);
+
+ $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
+ $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
+ $('.todo').on('click', this.goToTodoUrl);
+ }
+
+ initFilters() {
+ this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
+ this.initFilterDropdown($('.js-type-search'), 'type');
+ this.initFilterDropdown($('.js-action-search'), 'action_id');
+
+ $('form.filter-form').on('submit', function applyFilters(event) {
+ event.preventDefault();
+ gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
+ });
+ return new UsersSelect();
+ }
+
+ initFilterDropdown($dropdown, fieldName, searchFields) {
+ $dropdown.glDropdown({
+ fieldName,
+ selectable: true,
+ filterable: searchFields ? true : false,
+ search: { fields: searchFields },
+ data: $dropdown.data('data'),
+ clicked: () => $dropdown.closest('form.filter-form').submit(),
+ });
+ }
+
+ updateRowStateClicked(e) {
+ e.preventDefault();
+
+ const target = e.target;
+ target.setAttribute('disabled', true);
+ target.classList.add('disabled');
+ $.ajax({
+ type: 'POST',
+ url: target.dataset.href,
+ dataType: 'json',
+ data: {
+ '_method': target.dataset.method,
+ },
+ success: (data) => {
+ this.updateRowState(target);
+ return this.updateBadges(data);
+ },
+ });
+ }
+
+ updateRowState(target) {
+ const row = target.closest('li');
+ const restoreBtn = row.querySelector('.js-undo-todo');
+ const doneBtn = row.querySelector('.js-done-todo');
+
+ target.classList.add('hidden');
+ target.removeAttribute('disabled');
+ target.classList.remove('disabled');
+
+ if (target === doneBtn) {
+ row.classList.add('done-reversible');
+ restoreBtn.classList.remove('hidden');
+ } else if (target === restoreBtn) {
+ row.classList.remove('done-reversible');
+ doneBtn.classList.remove('hidden');
+ } else {
+ row.parentNode.removeChild(row);
+ }
+ }
+
+ updateAllStateClicked(e) {
+ e.preventDefault();
+
+ const target = e.currentTarget;
+ const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
+ target.setAttribute('disabled', true);
+ target.classList.add('disabled');
+ $.ajax({
+ type: 'POST',
+ url: target.dataset.href,
+ dataType: 'json',
+ data: requestData,
+ success: (data) => {
+ this.updateAllState(target, data);
+ return this.updateBadges(data);
+ },
+ });
+ }
+
+ updateAllState(target, data) {
+ const markAllDoneBtn = document.querySelector('.js-todos-mark-all');
+ const undoAllBtn = document.querySelector('.js-todos-undo-all');
+ const todoListContainer = document.querySelector('.js-todos-list-container');
+ const nothingHereContainer = document.querySelector('.js-nothing-here-container');
+
+ target.removeAttribute('disabled');
+ target.classList.remove('disabled');
+
+ this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
+ undoAllBtn.classList.toggle('hidden');
+ markAllDoneBtn.classList.toggle('hidden');
+ todoListContainer.classList.toggle('hidden');
+ nothingHereContainer.classList.toggle('hidden');
+ }
+
+ updateBadges(data) {
+ $(document).trigger('todo:toggle', data.count);
+ document.querySelector('.todos-pending .badge').innerHTML = data.count;
+ document.querySelector('.todos-done .badge').innerHTML = data.done_count;
+ }
+
+ goToTodoUrl(e) {
+ const todoLink = this.dataset.url;
+
+ if (!todoLink) {
+ return;
+ }
+
+ if (gl.utils.isMetaClick(e)) {
+ const windowTarget = '_blank';
+ const selected = e.target;
+ e.preventDefault();
+
+ if (selected.tagName === 'IMG') {
+ const avatarUrl = selected.parentElement.getAttribute('href');
+ window.open(avatarUrl, windowTarget);
+ } else {
+ window.open(todoLink, windowTarget);
+ }
+ } else {
+ gl.utils.visitUrl(todoLink);
+ }
+ }
+}
+
+window.gl = window.gl || {};
+gl.Todos = Todos;
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
deleted file mode 100644
index e9513725d9d..00000000000
--- a/app/assets/javascripts/todos.js.es6
+++ /dev/null
@@ -1,146 +0,0 @@
-/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
-/* global UsersSelect */
-
-((global) => {
- class Todos {
- constructor() {
- this.initFilters();
- this.bindEvents();
-
- this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('beforeunload', this.cleanupWrapper);
- }
-
- cleanup() {
- this.unbindEvents();
- document.removeEventListener('beforeunload', this.cleanupWrapper);
- }
-
- unbindEvents() {
- $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
- $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
- $('.todo').off('click', this.goToTodoUrl);
- }
-
- bindEvents() {
- this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
- this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
-
- $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
- $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
- $('.todo').on('click', this.goToTodoUrl);
- }
-
- initFilters() {
- new UsersSelect();
- this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
- this.initFilterDropdown($('.js-type-search'), 'type');
- this.initFilterDropdown($('.js-action-search'), 'action_id');
-
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
- });
- }
-
- initFilterDropdown($dropdown, fieldName, searchFields) {
- $dropdown.glDropdown({
- fieldName,
- selectable: true,
- filterable: searchFields ? true : false,
- search: { fields: searchFields },
- data: $dropdown.data('data'),
- clicked: function () {
- return $dropdown.closest('form.filter-form').submit();
- },
- });
- }
-
- updateStateClicked(e) {
- e.preventDefault();
- const target = e.target;
- target.setAttribute('disabled', '');
- target.classList.add('disabled');
- $.ajax({
- type: 'POST',
- url: target.getAttribute('href'),
- dataType: 'json',
- data: {
- '_method': target.getAttribute('data-method'),
- },
- success: (data) => {
- this.updateState(target);
- this.updateBadges(data);
- },
- });
- }
-
- allDoneClicked(e) {
- e.preventDefault();
- const $target = $(e.currentTarget);
- $target.disable();
- $.ajax({
- type: 'POST',
- url: $target.attr('href'),
- dataType: 'json',
- data: {
- '_method': 'delete',
- },
- success: (data) => {
- $target.remove();
- $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
- this.updateBadges(data);
- },
- });
- }
-
- updateState(target) {
- const row = target.closest('li');
- const restoreBtn = row.querySelector('.js-undo-todo');
- const doneBtn = row.querySelector('.js-done-todo');
-
- target.removeAttribute('disabled');
- target.classList.remove('disabled');
- target.classList.add('hidden');
-
- if (target === doneBtn) {
- row.classList.add('done-reversible');
- restoreBtn.classList.remove('hidden');
- } else {
- row.classList.remove('done-reversible');
- doneBtn.classList.remove('hidden');
- }
- }
-
- updateBadges(data) {
- $(document).trigger('todo:toggle', data.count);
- $('.todos-pending .badge').text(data.count);
- $('.todos-done .badge').text(data.done_count);
- }
-
- goToTodoUrl(e) {
- const todoLink = this.dataset.url;
-
- if (!todoLink) {
- return;
- }
-
- if (gl.utils.isMetaClick(e)) {
- const windowTarget = '_blank';
- const selected = e.target;
- e.preventDefault();
-
- if (selected.tagName === 'IMG') {
- const avatarUrl = selected.parentElement.getAttribute('href');
- window.open(avatarUrl, windowTarget);
- } else {
- window.open(todoLink, windowTarget);
- }
- } else {
- gl.utils.visitUrl(todoLink);
- }
- }
- }
-
- global.Todos = Todos;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js
index 500b78fc5d8..500b78fc5d8 100644
--- a/app/assets/javascripts/u2f/authenticate.js.es6
+++ b/app/assets/javascripts/u2f/authenticate.js
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js
index 059e6c628b3..059e6c628b3 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
new file mode 100644
index 00000000000..99419e85b20
--- /dev/null
+++ b/app/assets/javascripts/user_callout.js
@@ -0,0 +1,60 @@
+/* global Cookies */
+
+const userCalloutElementName = '.user-callout';
+const closeButton = '.close-user-callout';
+const userCalloutBtn = '.user-callout-btn';
+const userCalloutSvgAttrName = 'callout-svg';
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+
+const USER_CALLOUT_TEMPLATE = `
+ <div class="bordered-box landing content-block">
+ <button class="btn btn-default close close-user-callout" type="button">
+ <i class="fa fa-times dismiss-icon"></i>
+ </button>
+ <div class="row">
+ <div class="col-sm-3 col-xs-12 svg-container">
+ </div>
+ <div class="col-sm-8 col-xs-12 inner-content">
+ <h4>
+ Customize your experience
+ </h4>
+ <p>
+ Change syntax themes, default project pages, and more in preferences.
+ </p>
+ <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
+ </div>
+ </div>
+</div>`;
+
+class UserCallout {
+ constructor() {
+ this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
+ this.userCalloutBody = $(userCalloutElementName);
+ this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
+ $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+ this.init();
+ }
+
+ init() {
+ const $template = $(USER_CALLOUT_TEMPLATE);
+ if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
+ $template.find('.svg-container').append(this.userCalloutSvg);
+ this.userCalloutBody.append($template);
+ $template.find(closeButton).on('click', e => this.dismissCallout(e));
+ $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
+ } else {
+ this.userCalloutBody.remove();
+ }
+ }
+
+ dismissCallout(e) {
+ Cookies.set(USER_CALLOUT_COOKIE, 'true');
+ const $currentTarget = $(e.currentTarget);
+ if ($currentTarget.hasClass('close-user-callout')) {
+ this.userCalloutBody.remove();
+ }
+ }
+}
+
+module.exports = UserCallout;
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js
index 465618e3d53..465618e3d53 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js
index 137cefa3b8e..137cefa3b8e 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 5111b260e1c..754d448564f 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */
-/* global d3 */
+
+import d3 from 'd3';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 4cad60a59b1..580e2d84be5 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,3 +1 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
+require('./calendar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index de33a31b411..eb897e9dfe9 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -53,13 +53,22 @@
$loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () {
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
});
};
+ $('.assign-to-me-link').on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+ const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
+ $input.val(gon.current_user_id);
+ selectedId = $input.val();
+ $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
+ });
+
$block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault();
@@ -81,7 +90,7 @@
data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
@@ -199,15 +208,15 @@
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
+ if (selectedId === gon.current_user_id) {
+ $('.assign-to-me-link').hide();
+ } else {
+ $('.assign-to-me-link').show();
+ }
return;
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
- } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
- selectedId = user.id;
- gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
- gl.issueBoards.BoardsStore.updateFiltersUrl();
- e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
@@ -234,11 +243,16 @@
id: function (user) {
return user.id;
},
+ opened: function(e) {
+ const $el = $(e.currentTarget);
+ $el.find('.is-active').removeClass('is-active');
+ $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
+ },
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : "";
avatar = user.avatar_url ? user.avatar_url : false;
- selected = user.id === selectedId ? "is-active" : "";
+ selected = user.id === parseInt(selectedId, 10) ? "is-active" : "";
img = "";
if (user.beforeDivider != null) {
"<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>";
@@ -248,7 +262,7 @@
}
}
// split into three parts so we can remove the username section if nessesary
- listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
+ listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
listClosingTags = "</a> </li>";
if (username === '') {
diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js
new file mode 100644
index 00000000000..d4f716acb72
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js
@@ -0,0 +1,10 @@
+class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
+ }
+}
+
+window.gl = window.gl || {};
+gl.VersionCheckImage = VersionCheckImage;
+
+module.exports = VersionCheckImage;
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
deleted file mode 100644
index 1fa2b5ac399..00000000000
--- a/app/assets/javascripts/version_check_image.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-(() => {
- class VersionCheckImage {
- static bindErrorEvent(imageElement) {
- imageElement.off('error').on('error', () => imageElement.hide());
- }
- }
-
- window.gl = window.gl || {};
- gl.VersionCheckImage = VersionCheckImage;
-})();
diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js
index f712d7ba930..f712d7ba930 100644
--- a/app/assets/javascripts/visibility_select.js.es6
+++ b/app/assets/javascripts/visibility_select.js
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 00000000000..aaebf29d8ae
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ },
+
+ icon: {
+ type: String,
+ required: true,
+ },
+
+ cssClass: {
+ type: String,
+ required: true,
+ },
+
+ confirmActionMessage: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ iconClass() {
+ return `fa fa-${this.icon}`;
+ },
+
+ buttonClass() {
+ return `btn has-tooltip ${this.cssClass}`;
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+ this.makeRequest();
+ } else if (!this.confirmActionMessage) {
+ this.makeRequest();
+ }
+ },
+
+ makeRequest() {
+ this.isLoading = true;
+
+ this.service.postAction(this.endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-placement="top"
+ :disabled="isLoading">
+ <i :class="iconClass" aria-hidden="true"/>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 00000000000..4e183d5c8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ class="js-pipeline-url-user"
+ v-if="user"
+ :href="pipeline.user.web_url">
+ <img
+ v-if="user"
+ class="avatar has-tooltip s20 "
+ :title="pipeline.user.name"
+ data-container="body"
+ :src="pipeline.user.avatar_url"
+ >
+ </a>
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api monospace">
+ API
+ </span>
+ <span
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-lastest label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch">
+ latest
+ </span>
+ <span
+ v-if="pipeline.flags.yaml_errors"
+ class="js-pipeline-url-yaml label label-danger has-tooltip"
+ :title="pipeline.yaml_errors"
+ :data-original-title="pipeline.yaml_errors">
+ yaml invalid
+ </span>
+ <span
+ v-if="pipeline.flags.stuck"
+ class="js-pipeline-url-stuck label label-warning">
+ stuck
+ </span>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 00000000000..4bb2b048884
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" v-if="actions">
+ <button
+ type="button"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ title="Manual job"
+ data-toggle="dropdown"
+ data-placement="top"
+ aria-label="Manual job"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-pipeline-action-link no-btn"
+ @click="onClickAction(action.path)">
+ ${playIconSvg}
+ <span>{{action.name}}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 00000000000..3555040d60f
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,32 @@
+export default {
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="artifact in artifacts">
+ <a
+ rel="nofollow"
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>Download {{artifact.name}} artifacts</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 00000000000..a2c29002707
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ svg: svgsDictionary[this.stage.status.icon],
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label="stage.title">
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 00000000000..21a281af438
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ svg: svgsDictionary[this.pipeline.details.status.icon],
+ };
+ },
+
+ computed: {
+ cssClasses() {
+ return `ci-status ci-${this.pipeline.details.status.group}`;
+ },
+
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+
+ content() {
+ return `${this.svg} ${this.pipeline.details.status.text}`;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class="cssClasses"
+ :href="detailsPath"
+ v-html="content">
+ </a>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 00000000000..498d0715f54
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ currentTime: new Date(),
+ iconTimerSvg,
+ };
+ },
+ props: ['pipeline'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td class="pipelines-time-ago">
+ <p class="duration" v-if='duration'>
+ <span v-html="iconTimerSvg"></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'>
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
new file mode 100644
index 00000000000..b4e2d3a1143
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -0,0 +1,28 @@
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+
+$(() => new Vue({
+ el: document.querySelector('.vue-pipelines-index'),
+
+ data() {
+ const project = document.querySelector('.pipelines');
+ const store = new PipelinesStore();
+
+ return {
+ store,
+ endpoint: project.dataset.url,
+ };
+ },
+ components: {
+ 'vue-pipelines': PipelinesComponent,
+ },
+ template: `
+ <vue-pipelines
+ :endpoint="endpoint"
+ :store="store" />
+ `,
+}));
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
deleted file mode 100644
index e7432afb56e..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ /dev/null
@@ -1,36 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
-
-$(() => new Vue({
- el: document.querySelector('.vue-pipelines-index'),
-
- data() {
- const project = document.querySelector('.pipelines');
- const svgs = document.querySelector('.pipeline-svgs').dataset;
-
- // Transform svgs DOMStringMap to a plain Object.
- const svgsObject = gl.utils.DOMStringMapToObject(svgs);
-
- return {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
- svgs: svgsObject,
- };
- },
- components: {
- 'vue-pipelines': gl.VuePipelines,
- },
- template: `
- <vue-pipelines
- :scope='scope'
- :store='store'
- :svgs='svgs'
- >
- </vue-pipelines>
- `,
-}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
deleted file mode 100644
index 54e8f977a47..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ /dev/null
@@ -1,105 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineActions = Vue.extend({
- props: ['pipeline', 'svgs'],
- computed: {
- actions() {
- return this.pipeline.details.manual_actions.length > 0;
- },
- artifacts() {
- return this.pipeline.details.artifacts.length > 0;
- },
- },
- methods: {
- download(name) {
- return `Download ${name} artifacts`;
- },
- },
- template: `
- <td class="pipeline-actions hidden-xs">
- <div class="controls pull-right">
- <div class="btn-group inline">
- <div class="btn-group">
- <button
- v-if='actions'
- class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
- data-toggle="dropdown"
- title="Manual job"
- data-placement="top"
- aria-label="Manual job"
- >
- <span v-html='svgs.iconPlay' aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='action in pipeline.details.manual_actions'>
- <a
- rel="nofollow"
- data-method="post"
- :href='action.path'
- >
- <span v-html='svgs.iconPlay' aria-hidden="true"></span>
- <span>{{action.name}}</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="btn-group">
- <button
- v-if='artifacts'
- class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- title="Artifacts"
- data-placement="top"
- data-toggle="dropdown"
- aria-label="Artifacts"
- >
- <i class="fa fa-download" aria-hidden="true"></i>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='artifact in pipeline.details.artifacts'>
- <a
- rel="nofollow"
- download
- :href='artifact.path'
- >
- <i class="fa fa-download" aria-hidden="true"></i>
- <span>{{download(artifact.name)}}</span>
- </a>
- </li>
- </ul>
- </div>
- </div>
- <div class="cancel-retry-btns inline">
- <a
- v-if='pipeline.flags.retryable'
- class="btn has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- <a
- v-if='pipeline.flags.cancelable'
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
- </div>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
deleted file mode 100644
index ae5649f0519..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineUrl = Vue.extend({
- props: [
- 'pipeline',
- ],
- computed: {
- user() {
- return !!this.pipeline.user;
- },
- },
- template: `
- <td>
- <a :href='pipeline.path'>
- <span class="pipeline-id">#{{pipeline.id}}</span>
- </a>
- <span>by</span>
- <a
- v-if='user'
- :href='pipeline.user.web_url'
- >
- <img
- v-if='user'
- class="avatar has-tooltip s20 "
- :title='pipeline.user.name'
- data-container="body"
- :src='pipeline.user.avatar_url'
- >
- </a>
- <span
- v-if='!user'
- class="api monospace"
- >
- API
- </span>
- <span
- v-if='pipeline.flags.latest'
- class="label label-success has-tooltip"
- title="Latest pipeline for this branch"
- data-original-title="Latest pipeline for this branch"
- >
- latest
- </span>
- <span
- v-if='pipeline.flags.yaml_errors'
- class="label label-danger has-tooltip"
- :title='pipeline.yaml_errors'
- :data-original-title='pipeline.yaml_errors'
- >
- yaml invalid
- </span>
- <span
- v-if='pipeline.flags.stuck'
- class="label label-warning"
- >
- stuck
- </span>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
new file mode 100644
index 00000000000..f389e5e4950
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -0,0 +1,121 @@
+/* global Flash */
+/* eslint-disable no-new */
+import '~/flash';
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'gl-pagination': TablePaginationComponent,
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
+
+ data() {
+ return {
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ pageRequest: false,
+ };
+ },
+
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
+ },
+
+ fetchPipelines() {
+ const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+ const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+ this.pageRequest = true;
+ return this.service.getPipelines(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+ })
+ .then(() => {
+ this.pageRequest = false;
+ })
+ .catch(() => {
+ this.pageRequest = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
+ },
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="pageRequest">
+ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
+ </div>
+
+ <gl-pagination
+ v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"
+ >
+ </gl-pagination>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
deleted file mode 100644
index 0265c00a414..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ /dev/null
@@ -1,93 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
-
-((gl) => {
- gl.VuePipelines = Vue.extend({
-
- components: {
- 'gl-pagination': gl.VueGlPagination,
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
- },
-
- data() {
- return {
- pipelines: [],
- timeLoopInterval: '',
- intervalId: '',
- apiScope: 'all',
- pageInfo: {},
- pagenum: 1,
- count: { all: 0, running_or_pending: 0 },
- pageRequest: false,
- };
- },
- props: ['scope', 'store', 'svgs'],
- created() {
- const pagenum = gl.utils.getParameterByName('page');
- const scope = gl.utils.getParameterByName('scope');
- if (pagenum) this.pagenum = pagenum;
- if (scope) this.apiScope = scope;
-
- this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
- },
-
- beforeUpdate() {
- if (this.pipelines.length && this.$children) {
- CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
- }
- },
-
- methods: {
- /**
- * Changes the URL according to the pagination component.
- *
- * If no scope is provided, 'all' is assumed.
- *
- * Pagination component sends "null" when no scope is provided.
- *
- * @param {Number} pagenum
- * @param {String} apiScope = 'all'
- */
- change(pagenum, apiScope) {
- if (!apiScope) apiScope = 'all';
- gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
- },
- },
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
-
- <div class="table-holder" v-if='!pageRequest && pipelines.length'>
- <pipelines-table-component
- :pipelines='pipelines'
- :svgs='svgs'>
- </pipelines-table-component>
- </div>
-
- <gl-pagination
- v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
- :pagenum='pagenum'
- :change='change'
- :count='count.all'
- :pageInfo='pageInfo'
- >
- </gl-pagination>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 00000000000..708f5068dd3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(scope, page) {
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
deleted file mode 100644
index 8cc417a9966..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ /dev/null
@@ -1,103 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueStage = Vue.extend({
- data() {
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- };
- },
- props: {
- stage: {
- type: Object,
- required: true,
- },
- svgs: {
- type: Object,
- required: true,
- },
- match: {
- type: Function,
- required: true,
- },
- },
- methods: {
- fetchBuilds(e) {
- const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
- keepGraph(e) {
- const { target } = e;
-
- if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
-
- if (
- target.parentElement &&
- (target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
- ) return null;
-
- return e.stopPropagation();
- },
- },
- computed: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- svg() {
- const { icon } = this.stage.status;
- const stageIcon = icon.replace(/icon/i, 'stage_icon');
- return this.svgs[this.match(stageIcon)];
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- },
- template: `
- <div>
- <button
- @click='fetchBuilds($event)'
- :class="triggerButtonClass"
- :title='stage.title'
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- :aria-label='stage.title'
- >
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- @click='keepGraph($event)'
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner"
- >
- </div>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6
deleted file mode 100644
index 05175082704..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js.es6
+++ /dev/null
@@ -1,34 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueStatusScope = Vue.extend({
- props: [
- 'pipeline', 'svgs', 'match',
- ],
- computed: {
- cssClasses() {
- const cssObject = { 'ci-status': true };
- cssObject[`ci-${this.pipeline.details.status.group}`] = true;
- return cssObject;
- },
- svg() {
- return this.svgs[this.match(this.pipeline.details.status.icon)];
- },
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class='cssClasses'
- :href='detailsPath'
- v-html='svg + pipeline.details.status.text'
- >
- </a>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
deleted file mode 100644
index 909007267b9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.parseIntPagination(normalized);
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- this.pageRequest = true;
-
- return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
- .then((response) => {
- const pageInfo = pageValues(response.headers);
- this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
- const res = JSON.parse(response.body);
- this.count = Object.assign({}, this.count, res.count);
- this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
- });
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
new file mode 100644
index 00000000000..7ac10086a55
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -0,0 +1,61 @@
+/* eslint-disable no-underscore-dangle*/
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
+ constructor() {
+ this.state = {};
+
+ this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+ }
+
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
+ }
+
+ /**
+ * FIXME: Move this inside the component.
+ *
+ * Once the data is received we will start the time ago loops.
+ *
+ * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
+ * update the time to show how long as passed.
+ *
+ */
+ startTimeAgoLoops() {
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
deleted file mode 100644
index 3598da11573..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ /dev/null
@@ -1,76 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-((gl) => {
- gl.VueTimeAgo = Vue.extend({
- data() {
- return {
- currentTime: new Date(),
- };
- },
- props: ['pipeline', 'svgs'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td>
- <p class="duration" v-if='duration'>
- <span v-html='svgs.iconTimer'></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'
- >
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js
index 30f6680a673..30f6680a673 100644
--- a/app/assets/javascripts/vue_realtime_listener/index.js.es6
+++ b/app/assets/javascripts/vue_realtime_listener/index.js
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
new file mode 100644
index 00000000000..fb68abd95a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -0,0 +1,157 @@
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
deleted file mode 100644
index ff88e236829..00000000000
--- a/app/assets/javascripts/vue_shared/components/commit.js.es6
+++ /dev/null
@@ -1,164 +0,0 @@
-/* global Vue */
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- commitIconSvg: {
- type: String,
- required: false,
- },
- },
-
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
- },
-
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
new file mode 100644
index 00000000000..afd8d7acf6b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -0,0 +1,48 @@
+import PipelinesTableRowComponent from './pipelines_table_row';
+
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'pipelines-table-row-component': PipelinesTableRowComponent,
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
deleted file mode 100644
index 4bdaef31ee9..00000000000
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('./pipelines_table_row');
-/**
- * Pipelines Table Component.
- *
- * Given an array of objects, renders a table.
- */
-
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
- props: {
- pipelines: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
- /**
- * TODO: Remove this when we have webpack.
- */
- svgs: {
- type: Object,
- required: true,
- default: () => ({}),
- },
- },
-
- components: {
- 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
- },
-
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="js-pipeline-status pipeline-status">Status</th>
- <th class="js-pipeline-info pipeline-info">Pipeline</th>
- <th class="js-pipeline-commit pipeline-commit">Commit</th>
- <th class="js-pipeline-stages pipeline-stages">Stages</th>
- <th class="js-pipeline-date pipeline-date"></th>
- <th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in pipelines"
- v-bind:model="model">
- <tr is="pipelines-table-row-component"
- :pipeline="model"
- :svgs="svgs"></tr>
- </template>
- </tbody>
- </table>
- `,
- });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
new file mode 100644
index 00000000000..f5b3cb9214e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -0,0 +1,228 @@
+/* eslint-disable no-param-reassign */
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
+
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage :stage="stage"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
deleted file mode 100644
index 61c1b72d9d2..00000000000
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
+++ /dev/null
@@ -1,234 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
-/**
- * Pipeline table row.
- *
- * Given the received object renders a table row in the pipelines' table.
- */
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
- props: {
- pipeline: {
- type: Object,
- required: true,
- default: () => ({}),
- },
-
- /**
- * TODO: Remove this when we have webpack;
- */
- svgs: {
- type: Object,
- required: true,
- default: () => ({}),
- },
- },
-
- components: {
- 'commit-component': gl.CommitComponent,
- 'pipeline-actions': gl.VuePipelineActions,
- 'dropdown-stage': gl.VueStage,
- 'pipeline-url': gl.VuePipelineUrl,
- 'status-scope': gl.VueStatusScope,
- 'time-ago': gl.VueTimeAgo,
- },
-
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
-
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline &&
- this.pipeline.commit &&
- this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
-
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
- }
- }
-
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- if (this.pipeline &&
- this.pipeline.commit) {
- commitAuthorInformation = {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
- }
-
- return commitAuthorInformation;
- },
-
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
-
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
- },
-
- methods: {
- /**
- * FIXME: This should not be in this component but in the components that
- * need this function.
- *
- * Used to render SVGs in the following components:
- * - status-scope
- * - dropdown-stage
- *
- * @param {String} string
- * @return {String}
- */
- match(string) {
- return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
- },
- },
-
- template: `
- <tr class="commit">
- <status-scope
- :pipeline="pipeline"
- :svgs="svgs"
- :match="match">
- </status-scope>
-
- <pipeline-url :pipeline="pipeline"></pipeline-url>
-
- <td>
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"
- :commit-icon-svg="svgs.commitIconSvg">
- </commit-component>
- </td>
-
- <td class="stage-cell">
- <div class="stage-container dropdown js-mini-pipeline-graph"
- v-if="pipeline.details.stages.length > 0"
- v-for="stage in pipeline.details.stages">
- <dropdown-stage
- :stage="stage"
- :svgs="svgs"
- :match="match">
- </dropdown-stage>
- </div>
- </td>
-
- <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
-
- <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
- </tr>
- `,
- });
-})();
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
new file mode 100644
index 00000000000..b9cd28f6249
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -0,0 +1,135 @@
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '<< First';
+const LAST = 'Last >>';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
+ },
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
+ },
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
+
+ if (page > 1) items.push({ title: FIRST });
+
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
+
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
+
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
+
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
+
+ if (total - page >= 1) items.push({ title: LAST, last: true });
+
+ return items;
+ },
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
deleted file mode 100644
index d8042a9b7fc..00000000000
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
+++ /dev/null
@@ -1,150 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
- const PAGINATION_UI_BUTTON_LIMIT = 4;
- const UI_LIMIT = 6;
- const SPREAD = '...';
- const PREV = 'Prev';
- const NEXT = 'Next';
- const FIRST = '<< First';
- const LAST = 'Last >>';
-
- gl.VueGlPagination = Vue.extend({
- props: {
-
- // TODO: Consider refactoring in light of turbolinks removal.
-
- /**
- This function will take the information given by the pagination component
- And make a new Turbolinks call
-
- Here is an example `change` method:
-
- change(pagenum, apiScope) {
- gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
- },
- */
-
- change: {
- type: Function,
- required: true,
- },
-
- /**
- pageInfo will come from the headers of the API call
- in the `.then` clause of the VueResource API call
- there should be a function that contructs the pageInfo for this component
-
- This is an example:
-
- const pageInfo = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
- */
-
- pageInfo: {
- type: Object,
- required: true,
- },
- },
- methods: {
- changePage(e) {
- const apiScope = gl.utils.getParameterByName('scope');
-
- const text = e.target.innerText;
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages, apiScope);
- break;
- case NEXT:
- this.change(nextPage, apiScope);
- break;
- case PREV:
- this.change(previousPage, apiScope);
- break;
- case FIRST:
- this.change(1, apiScope);
- break;
- default:
- this.change(+text, apiScope);
- break;
- }
- },
- },
- computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
- const items = [];
-
- if (page > 1) items.push({ title: FIRST });
-
- if (page > 1) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
-
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
-
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
-
- for (let i = start; i <= end; i++) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
-
- if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
-
- if (page === total) {
- items.push({ title: NEXT, disabled: true, next: true });
- } else if (total - page >= 1) {
- items.push({ title: NEXT, next: true });
- }
-
- if (total - page >= 1) items.push({ title: LAST, last: true });
-
- return items;
- },
- },
- template: `
- <div class="gl-pagination">
- <ul class="pagination clearfix">
- <li v-for='item in getItems'
- :class='{
- page: item.page,
- prev: item.prev,
- next: item.next,
- separator: item.separator,
- active: item.active,
- disabled: item.disabled
- }'
- >
- <a @click="changePage($event)">{{item.title}}</a>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
new file mode 100644
index 00000000000..f1c1e553b16
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -0,0 +1,21 @@
+/* eslint-disable no-param-reassign, no-plusplus */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next(() => {
+ Vue.activeResources--;
+ });
+});
+
+Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+});
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
deleted file mode 100644
index d3229f9f730..00000000000
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
-
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next((response) => {
- if (typeof response.data === 'string') {
- response.data = JSON.parse(response.data);
- }
-
- Vue.activeResources--;
- });
-});
-
-Vue.http.interceptors.push((request, next) => {
- // needed in order to not break the tests.
- if ($.rails) {
- request.headers['X-CSRF-Token'] = $.rails.csrfToken();
- }
- next();
-});
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js
index 75fd1394a03..75fd1394a03 100644
--- a/app/assets/javascripts/wikis.js.es6
+++ b/app/assets/javascripts/wikis.js
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 1dcd1f8a6fc..83a8eeaafde 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
* 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/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 39cf3b5f8ae..5bb7e8caec1 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,5 +44,6 @@
@import "framework/images.scss";
@import "framework/broadcast-messages";
@import "framework/emojis.scss";
+@import "framework/emoji-sprites.scss";
@import "framework/icons.scss";
@import "framework/snippets.scss";
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 49907417e26..546718ddaf8 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -7,6 +7,7 @@
.emoji-menu {
position: absolute;
+ top: 0;
margin-top: 3px;
padding: $gl-padding;
z-index: 9;
@@ -20,7 +21,7 @@
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
- transition: .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition: .3s cubic-bezier(.67,.06,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
@@ -47,12 +48,13 @@
}
.emoji-menu-list {
- list-style: none;
- padding-left: 0;
margin-bottom: 0;
+ padding-left: 0;
+ list-style: none;
}
.emoji-menu-list-item {
+ float: left;
padding: 3px;
margin-left: 1px;
margin-right: 1px;
@@ -94,7 +96,7 @@
.award-control {
margin: 3px 5px 3px 0;
- padding: 5px 6px;
+ padding: .35em .4em;
outline: 0;
&.disabled {
@@ -136,10 +138,12 @@
}
.icon,
+ gl-emoji,
.award-control-icon {
- float: left;
- margin-right: 5px;
- font-size: 18px;
+ vertical-align: middle;
+ margin-right: 0.15em;
+ font-size: 1.5em;
+ line-height: 1;
}
.award-control-icon-loading {
@@ -150,4 +154,8 @@
color: $border-gray-normal;
margin-top: 1px;
}
+
+ .award-control-text {
+ vertical-align: middle;
+ }
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0f9213b98e3..9a4129cdc8d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -229,7 +229,7 @@
.controls {
float: right;
margin-top: 8px;
- padding-bottom: 7px;
+ padding-bottom: 8px;
border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index fb8ea18d122..9a0f7a14e57 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,6 +1,7 @@
.calender-block {
padding-left: 0;
padding-right: 0;
+ border-top: 0;
direction: rtl;
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbd..2c33b235980 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
@include str-truncated(100%);
}
}
+
+.tooltip {
+ .tooltip-inner {
+ word-wrap: break-word;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ff31e7f7b3d..186bb9ac616 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
white-space: nowrap;
&[disabled] {
- background-color: $input-bg-disabled;
+ opacity: .65;
cursor: not-allowed;
}
@@ -96,7 +96,7 @@
.dropdown-menu-toggle {
@extend .dropdown-toggle;
- padding-right: 20px;
+ padding-right: 25px;
position: relative;
width: 163px;
text-overflow: ellipsis;
@@ -107,11 +107,12 @@
&.fa-spinner {
font-size: 16px;
- margin-top: -8px;
+ margin-top: -3px;
}
}
- .fa-chevron-down {
+ .fa-chevron-down,
+ .fa-spinner {
position: absolute;
top: 11px;
right: 8px;
@@ -158,12 +159,12 @@
li {
text-align: left;
list-style: none;
- padding: 0 8px;
+ padding: 0 10px;
}
.divider {
height: 1px;
- margin: 8px;
+ margin: 6px 10px;
padding: 0;
background-color: $dropdown-divider-color;
}
@@ -180,7 +181,7 @@
display: block;
position: relative;
padding: 5px 8px;
- color: $dropdown-link-color;
+ color: $gl-text-color;
line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
@@ -192,6 +193,10 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
+
+ .badge {
+ background-color: darken($row-hover, 5%);
+ }
}
&.dropdown-menu-empty-link {
@@ -213,10 +218,12 @@
}
.dropdown-header {
- color: $gl-text-color-secondary;
+ color: $gl-text-color;
font-size: 13px;
+ font-weight: 600;
line-height: 22px;
- padding: 0 10px;
+ text-transform: capitalize;
+ padding: 0 16px;
}
.separator + .dropdown-header {
@@ -228,6 +235,12 @@
padding: 5px 8px;
color: $gl-text-color-secondary;
}
+
+ .badge {
+ position: absolute;
+ right: 8px;
+ top: 5px;
+ }
}
.dropdown-menu-drop-up {
@@ -313,14 +326,17 @@
.dropdown-menu-selectable {
a {
- padding-left: 25px;
+ padding-left: 26px;
&.is-indeterminate,
&.is-active {
+ font-weight: 600;
+ color: $gl-text-color;
+
&::before {
position: absolute;
- left: 5px;
- top: 8px;
+ left: 6px;
+ top: 6px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
@@ -342,7 +358,7 @@
.dropdown-title {
position: relative;
- padding: 0 25px 10px;
+ padding: 2px 25px 10px;
margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
@@ -372,7 +388,7 @@
right: 5px;
width: 20px;
height: 20px;
- top: -3px;
+ top: -1px;
}
.dropdown-menu-back {
diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss
new file mode 100644
index 00000000000..925415f84b1
--- /dev/null
+++ b/app/assets/stylesheets/framework/emoji-sprites.scss
@@ -0,0 +1,1811 @@
+.emoji-zzz { background-position: 0 0; }
+.emoji-1234 { background-position: -20px 0; }
+.emoji-1F627 { background-position: 0 -20px; }
+.emoji-8ball { background-position: -20px -20px; }
+.emoji-a { background-position: -40px 0; }
+.emoji-ab { background-position: -40px -20px; }
+.emoji-abc { background-position: 0 -40px; }
+.emoji-abcd { background-position: -20px -40px; }
+.emoji-accept { background-position: -40px -40px; }
+.emoji-aerial_tramway { background-position: -60px 0; }
+.emoji-airplane { background-position: -60px -20px; }
+.emoji-airplane_arriving { background-position: -60px -40px; }
+.emoji-airplane_departure { background-position: 0 -60px; }
+.emoji-airplane_small { background-position: -20px -60px; }
+.emoji-alarm_clock { background-position: -40px -60px; }
+.emoji-alembic { background-position: -60px -60px; }
+.emoji-alien { background-position: -80px 0; }
+.emoji-ambulance { background-position: -80px -20px; }
+.emoji-amphora { background-position: -80px -40px; }
+.emoji-anchor { background-position: -80px -60px; }
+.emoji-angel { background-position: 0 -80px; }
+.emoji-angel_tone1 { background-position: -20px -80px; }
+.emoji-angel_tone2 { background-position: -40px -80px; }
+.emoji-angel_tone3 { background-position: -60px -80px; }
+.emoji-angel_tone4 { background-position: -80px -80px; }
+.emoji-angel_tone5 { background-position: -100px 0; }
+.emoji-anger { background-position: -100px -20px; }
+.emoji-anger_right { background-position: -100px -40px; }
+.emoji-angry { background-position: -100px -60px; }
+.emoji-ant { background-position: -100px -80px; }
+.emoji-apple { background-position: 0 -100px; }
+.emoji-aquarius { background-position: -20px -100px; }
+.emoji-aries { background-position: -40px -100px; }
+.emoji-arrow_backward { background-position: -60px -100px; }
+.emoji-arrow_double_down { background-position: -80px -100px; }
+.emoji-arrow_double_up { background-position: -100px -100px; }
+.emoji-arrow_down { background-position: -120px 0; }
+.emoji-arrow_down_small { background-position: -120px -20px; }
+.emoji-arrow_forward { background-position: -120px -40px; }
+.emoji-arrow_heading_down { background-position: -120px -60px; }
+.emoji-arrow_heading_up { background-position: -120px -80px; }
+.emoji-arrow_left { background-position: -120px -100px; }
+.emoji-arrow_lower_left { background-position: 0 -120px; }
+.emoji-arrow_lower_right { background-position: -20px -120px; }
+.emoji-arrow_right { background-position: -40px -120px; }
+.emoji-arrow_right_hook { background-position: -60px -120px; }
+.emoji-arrow_up { background-position: -80px -120px; }
+.emoji-arrow_up_down { background-position: -100px -120px; }
+.emoji-arrow_up_small { background-position: -120px -120px; }
+.emoji-arrow_upper_left { background-position: -140px 0; }
+.emoji-arrow_upper_right { background-position: -140px -20px; }
+.emoji-arrows_clockwise { background-position: -140px -40px; }
+.emoji-arrows_counterclockwise { background-position: -140px -60px; }
+.emoji-art { background-position: -140px -80px; }
+.emoji-articulated_lorry { background-position: -140px -100px; }
+.emoji-asterisk { background-position: -140px -120px; }
+.emoji-astonished { background-position: 0 -140px; }
+.emoji-athletic_shoe { background-position: -20px -140px; }
+.emoji-atm { background-position: -40px -140px; }
+.emoji-atom { background-position: -60px -140px; }
+.emoji-avocado { background-position: -80px -140px; }
+.emoji-b { background-position: -100px -140px; }
+.emoji-baby { background-position: -120px -140px; }
+.emoji-baby_bottle { background-position: -140px -140px; }
+.emoji-baby_chick { background-position: -160px 0; }
+.emoji-baby_symbol { background-position: -160px -20px; }
+.emoji-baby_tone1 { background-position: -160px -40px; }
+.emoji-baby_tone2 { background-position: -160px -60px; }
+.emoji-baby_tone3 { background-position: -160px -80px; }
+.emoji-baby_tone4 { background-position: -160px -100px; }
+.emoji-baby_tone5 { background-position: -160px -120px; }
+.emoji-back { background-position: -160px -140px; }
+.emoji-bacon { background-position: 0 -160px; }
+.emoji-badminton { background-position: -20px -160px; }
+.emoji-baggage_claim { background-position: -40px -160px; }
+.emoji-balloon { background-position: -60px -160px; }
+.emoji-ballot_box { background-position: -80px -160px; }
+.emoji-ballot_box_with_check { background-position: -100px -160px; }
+.emoji-bamboo { background-position: -120px -160px; }
+.emoji-banana { background-position: -140px -160px; }
+.emoji-bangbang { background-position: -160px -160px; }
+.emoji-bank { background-position: -180px 0; }
+.emoji-bar_chart { background-position: -180px -20px; }
+.emoji-barber { background-position: -180px -40px; }
+.emoji-baseball { background-position: -180px -60px; }
+.emoji-basketball { background-position: -180px -80px; }
+.emoji-basketball_player { background-position: -180px -100px; }
+.emoji-basketball_player_tone1 { background-position: -180px -120px; }
+.emoji-basketball_player_tone2 { background-position: -180px -140px; }
+.emoji-basketball_player_tone3 { background-position: -180px -160px; }
+.emoji-basketball_player_tone4 { background-position: 0 -180px; }
+.emoji-basketball_player_tone5 { background-position: -20px -180px; }
+.emoji-bat { background-position: -40px -180px; }
+.emoji-bath { background-position: -60px -180px; }
+.emoji-bath_tone1 { background-position: -80px -180px; }
+.emoji-bath_tone2 { background-position: -100px -180px; }
+.emoji-bath_tone3 { background-position: -120px -180px; }
+.emoji-bath_tone4 { background-position: -140px -180px; }
+.emoji-bath_tone5 { background-position: -160px -180px; }
+.emoji-bathtub { background-position: -180px -180px; }
+.emoji-battery { background-position: -200px 0; }
+.emoji-beach { background-position: -200px -20px; }
+.emoji-beach_umbrella { background-position: -200px -40px; }
+.emoji-bear { background-position: -200px -60px; }
+.emoji-bed { background-position: -200px -80px; }
+.emoji-bee { background-position: -200px -100px; }
+.emoji-beer { background-position: -200px -120px; }
+.emoji-beers { background-position: -200px -140px; }
+.emoji-beetle { background-position: -200px -160px; }
+.emoji-beginner { background-position: -200px -180px; }
+.emoji-bell { background-position: 0 -200px; }
+.emoji-bellhop { background-position: -20px -200px; }
+.emoji-bento { background-position: -40px -200px; }
+.emoji-bicyclist { background-position: -60px -200px; }
+.emoji-bicyclist_tone1 { background-position: -80px -200px; }
+.emoji-bicyclist_tone2 { background-position: -100px -200px; }
+.emoji-bicyclist_tone3 { background-position: -120px -200px; }
+.emoji-bicyclist_tone4 { background-position: -140px -200px; }
+.emoji-bicyclist_tone5 { background-position: -160px -200px; }
+.emoji-bike { background-position: -180px -200px; }
+.emoji-bikini { background-position: -200px -200px; }
+.emoji-biohazard { background-position: -220px 0; }
+.emoji-bird { background-position: -220px -20px; }
+.emoji-birthday { background-position: -220px -40px; }
+.emoji-black_circle { background-position: -220px -60px; }
+.emoji-black_heart { background-position: -220px -80px; }
+.emoji-black_joker { background-position: -220px -100px; }
+.emoji-black_large_square { background-position: -220px -120px; }
+.emoji-black_medium_small_square { background-position: -220px -140px; }
+.emoji-black_medium_square { background-position: -220px -160px; }
+.emoji-black_nib { background-position: -220px -180px; }
+.emoji-black_small_square { background-position: -220px -200px; }
+.emoji-black_square_button { background-position: 0 -220px; }
+.emoji-blossom { background-position: -20px -220px; }
+.emoji-blowfish { background-position: -40px -220px; }
+.emoji-blue_book { background-position: -60px -220px; }
+.emoji-blue_car { background-position: -80px -220px; }
+.emoji-blue_heart { background-position: -100px -220px; }
+.emoji-blush { background-position: -120px -220px; }
+.emoji-boar { background-position: -140px -220px; }
+.emoji-bomb { background-position: -160px -220px; }
+.emoji-book { background-position: -180px -220px; }
+.emoji-bookmark { background-position: -200px -220px; }
+.emoji-bookmark_tabs { background-position: -220px -220px; }
+.emoji-books { background-position: -240px 0; }
+.emoji-boom { background-position: -240px -20px; }
+.emoji-boot { background-position: -240px -40px; }
+.emoji-bouquet { background-position: -240px -60px; }
+.emoji-bow { background-position: -240px -80px; }
+.emoji-bow_and_arrow { background-position: -240px -100px; }
+.emoji-bow_tone1 { background-position: -240px -120px; }
+.emoji-bow_tone2 { background-position: -240px -140px; }
+.emoji-bow_tone3 { background-position: -240px -160px; }
+.emoji-bow_tone4 { background-position: -240px -180px; }
+.emoji-bow_tone5 { background-position: -240px -200px; }
+.emoji-bowling { background-position: -240px -220px; }
+.emoji-boxing_glove { background-position: 0 -240px; }
+.emoji-boy { background-position: -20px -240px; }
+.emoji-boy_tone1 { background-position: -40px -240px; }
+.emoji-boy_tone2 { background-position: -60px -240px; }
+.emoji-boy_tone3 { background-position: -80px -240px; }
+.emoji-boy_tone4 { background-position: -100px -240px; }
+.emoji-boy_tone5 { background-position: -120px -240px; }
+.emoji-bread { background-position: -140px -240px; }
+.emoji-bride_with_veil { background-position: -160px -240px; }
+.emoji-bride_with_veil_tone1 { background-position: -180px -240px; }
+.emoji-bride_with_veil_tone2 { background-position: -200px -240px; }
+.emoji-bride_with_veil_tone3 { background-position: -220px -240px; }
+.emoji-bride_with_veil_tone4 { background-position: -240px -240px; }
+.emoji-bride_with_veil_tone5 { background-position: -260px 0; }
+.emoji-bridge_at_night { background-position: -260px -20px; }
+.emoji-briefcase { background-position: -260px -40px; }
+.emoji-broken_heart { background-position: -260px -60px; }
+.emoji-bug { background-position: -260px -80px; }
+.emoji-bulb { background-position: -260px -100px; }
+.emoji-bullettrain_front { background-position: -260px -120px; }
+.emoji-bullettrain_side { background-position: -260px -140px; }
+.emoji-burrito { background-position: -260px -160px; }
+.emoji-bus { background-position: -260px -180px; }
+.emoji-busstop { background-position: -260px -200px; }
+.emoji-bust_in_silhouette { background-position: -260px -220px; }
+.emoji-busts_in_silhouette { background-position: -260px -240px; }
+.emoji-butterfly { background-position: 0 -260px; }
+.emoji-cactus { background-position: -20px -260px; }
+.emoji-cake { background-position: -40px -260px; }
+.emoji-calendar { background-position: -60px -260px; }
+.emoji-calendar_spiral { background-position: -80px -260px; }
+.emoji-call_me { background-position: -100px -260px; }
+.emoji-call_me_tone1 { background-position: -120px -260px; }
+.emoji-call_me_tone2 { background-position: -140px -260px; }
+.emoji-call_me_tone3 { background-position: -160px -260px; }
+.emoji-call_me_tone4 { background-position: -180px -260px; }
+.emoji-call_me_tone5 { background-position: -200px -260px; }
+.emoji-calling { background-position: -220px -260px; }
+.emoji-camel { background-position: -240px -260px; }
+.emoji-camera { background-position: -260px -260px; }
+.emoji-camera_with_flash { background-position: -280px 0; }
+.emoji-camping { background-position: -280px -20px; }
+.emoji-cancer { background-position: -280px -40px; }
+.emoji-candle { background-position: -280px -60px; }
+.emoji-candy { background-position: -280px -80px; }
+.emoji-canoe { background-position: -280px -100px; }
+.emoji-capital_abcd { background-position: -280px -120px; }
+.emoji-capricorn { background-position: -280px -140px; }
+.emoji-card_box { background-position: -280px -160px; }
+.emoji-card_index { background-position: -280px -180px; }
+.emoji-carousel_horse { background-position: -280px -200px; }
+.emoji-carrot { background-position: -280px -220px; }
+.emoji-cartwheel { background-position: -280px -240px; }
+.emoji-cartwheel_tone1 { background-position: -280px -260px; }
+.emoji-cartwheel_tone2 { background-position: 0 -280px; }
+.emoji-cartwheel_tone3 { background-position: -20px -280px; }
+.emoji-cartwheel_tone4 { background-position: -40px -280px; }
+.emoji-cartwheel_tone5 { background-position: -60px -280px; }
+.emoji-cat { background-position: -80px -280px; }
+.emoji-cat2 { background-position: -100px -280px; }
+.emoji-cd { background-position: -120px -280px; }
+.emoji-chains { background-position: -140px -280px; }
+.emoji-champagne { background-position: -160px -280px; }
+.emoji-champagne_glass { background-position: -180px -280px; }
+.emoji-chart { background-position: -200px -280px; }
+.emoji-chart_with_downwards_trend { background-position: -220px -280px; }
+.emoji-chart_with_upwards_trend { background-position: -240px -280px; }
+.emoji-checkered_flag { background-position: -260px -280px; }
+.emoji-cheese { background-position: -280px -280px; }
+.emoji-cherries { background-position: -300px 0; }
+.emoji-cherry_blossom { background-position: -300px -20px; }
+.emoji-chestnut { background-position: -300px -40px; }
+.emoji-chicken { background-position: -300px -60px; }
+.emoji-children_crossing { background-position: -300px -80px; }
+.emoji-chipmunk { background-position: -300px -100px; }
+.emoji-chocolate_bar { background-position: -300px -120px; }
+.emoji-christmas_tree { background-position: -300px -140px; }
+.emoji-church { background-position: -300px -160px; }
+.emoji-cinema { background-position: -300px -180px; }
+.emoji-circus_tent { background-position: -300px -200px; }
+.emoji-city_dusk { background-position: -300px -220px; }
+.emoji-city_sunset { background-position: -300px -240px; }
+.emoji-cityscape { background-position: -300px -260px; }
+.emoji-cl { background-position: -300px -280px; }
+.emoji-clap { background-position: 0 -300px; }
+.emoji-clap_tone1 { background-position: -20px -300px; }
+.emoji-clap_tone2 { background-position: -40px -300px; }
+.emoji-clap_tone3 { background-position: -60px -300px; }
+.emoji-clap_tone4 { background-position: -80px -300px; }
+.emoji-clap_tone5 { background-position: -100px -300px; }
+.emoji-clapper { background-position: -120px -300px; }
+.emoji-classical_building { background-position: -140px -300px; }
+.emoji-clipboard { background-position: -160px -300px; }
+.emoji-clock { background-position: -180px -300px; }
+.emoji-clock1 { background-position: -200px -300px; }
+.emoji-clock10 { background-position: -220px -300px; }
+.emoji-clock1030 { background-position: -240px -300px; }
+.emoji-clock11 { background-position: -260px -300px; }
+.emoji-clock1130 { background-position: -280px -300px; }
+.emoji-clock12 { background-position: -300px -300px; }
+.emoji-clock1230 { background-position: -320px 0; }
+.emoji-clock130 { background-position: -320px -20px; }
+.emoji-clock2 { background-position: -320px -40px; }
+.emoji-clock230 { background-position: -320px -60px; }
+.emoji-clock3 { background-position: -320px -80px; }
+.emoji-clock330 { background-position: -320px -100px; }
+.emoji-clock4 { background-position: -320px -120px; }
+.emoji-clock430 { background-position: -320px -140px; }
+.emoji-clock5 { background-position: -320px -160px; }
+.emoji-clock530 { background-position: -320px -180px; }
+.emoji-clock6 { background-position: -320px -200px; }
+.emoji-clock630 { background-position: -320px -220px; }
+.emoji-clock7 { background-position: -320px -240px; }
+.emoji-clock730 { background-position: -320px -260px; }
+.emoji-clock8 { background-position: -320px -280px; }
+.emoji-clock830 { background-position: -320px -300px; }
+.emoji-clock9 { background-position: 0 -320px; }
+.emoji-clock930 { background-position: -20px -320px; }
+.emoji-closed_book { background-position: -40px -320px; }
+.emoji-closed_lock_with_key { background-position: -60px -320px; }
+.emoji-closed_umbrella { background-position: -80px -320px; }
+.emoji-cloud { background-position: -100px -320px; }
+.emoji-cloud_lightning { background-position: -120px -320px; }
+.emoji-cloud_rain { background-position: -140px -320px; }
+.emoji-cloud_snow { background-position: -160px -320px; }
+.emoji-cloud_tornado { background-position: -180px -320px; }
+.emoji-clown { background-position: -200px -320px; }
+.emoji-clubs { background-position: -220px -320px; }
+.emoji-cocktail { background-position: -240px -320px; }
+.emoji-coffee { background-position: -260px -320px; }
+.emoji-coffin { background-position: -280px -320px; }
+.emoji-cold_sweat { background-position: -300px -320px; }
+.emoji-comet { background-position: -320px -320px; }
+.emoji-compression { background-position: -340px 0; }
+.emoji-computer { background-position: -340px -20px; }
+.emoji-confetti_ball { background-position: -340px -40px; }
+.emoji-confounded { background-position: -340px -60px; }
+.emoji-confused { background-position: -340px -80px; }
+.emoji-congratulations { background-position: -340px -100px; }
+.emoji-construction { background-position: -340px -120px; }
+.emoji-construction_site { background-position: -340px -140px; }
+.emoji-construction_worker { background-position: -340px -160px; }
+.emoji-construction_worker_tone1 { background-position: -340px -180px; }
+.emoji-construction_worker_tone2 { background-position: -340px -200px; }
+.emoji-construction_worker_tone3 { background-position: -340px -220px; }
+.emoji-construction_worker_tone4 { background-position: -340px -240px; }
+.emoji-construction_worker_tone5 { background-position: -340px -260px; }
+.emoji-control_knobs { background-position: -340px -280px; }
+.emoji-convenience_store { background-position: -340px -300px; }
+.emoji-cookie { background-position: -340px -320px; }
+.emoji-cooking { background-position: 0 -340px; }
+.emoji-cool { background-position: -20px -340px; }
+.emoji-cop { background-position: -40px -340px; }
+.emoji-cop_tone1 { background-position: -60px -340px; }
+.emoji-cop_tone2 { background-position: -80px -340px; }
+.emoji-cop_tone3 { background-position: -100px -340px; }
+.emoji-cop_tone4 { background-position: -120px -340px; }
+.emoji-cop_tone5 { background-position: -140px -340px; }
+.emoji-copyright { background-position: -160px -340px; }
+.emoji-corn { background-position: -180px -340px; }
+.emoji-couch { background-position: -200px -340px; }
+.emoji-couple { background-position: -220px -340px; }
+.emoji-couple_mm { background-position: -240px -340px; }
+.emoji-couple_with_heart { background-position: -260px -340px; }
+.emoji-couple_ww { background-position: -280px -340px; }
+.emoji-couplekiss { background-position: -300px -340px; }
+.emoji-cow { background-position: -320px -340px; }
+.emoji-cow2 { background-position: -340px -340px; }
+.emoji-cowboy { background-position: -360px 0; }
+.emoji-crab { background-position: -360px -20px; }
+.emoji-crayon { background-position: -360px -40px; }
+.emoji-credit_card { background-position: -360px -60px; }
+.emoji-crescent_moon { background-position: -360px -80px; }
+.emoji-cricket { background-position: -360px -100px; }
+.emoji-crocodile { background-position: -360px -120px; }
+.emoji-croissant { background-position: -360px -140px; }
+.emoji-cross { background-position: -360px -160px; }
+.emoji-crossed_flags { background-position: -360px -180px; }
+.emoji-crossed_swords { background-position: -360px -200px; }
+.emoji-crown { background-position: -360px -220px; }
+.emoji-cruise_ship { background-position: -360px -240px; }
+.emoji-cry { background-position: -360px -260px; }
+.emoji-crying_cat_face { background-position: -360px -280px; }
+.emoji-crystal_ball { background-position: -360px -300px; }
+.emoji-cucumber { background-position: -360px -320px; }
+.emoji-cupid { background-position: -360px -340px; }
+.emoji-curly_loop { background-position: 0 -360px; }
+.emoji-currency_exchange { background-position: -20px -360px; }
+.emoji-curry { background-position: -40px -360px; }
+.emoji-custard { background-position: -60px -360px; }
+.emoji-customs { background-position: -80px -360px; }
+.emoji-cyclone { background-position: -100px -360px; }
+.emoji-dagger { background-position: -120px -360px; }
+.emoji-dancer { background-position: -140px -360px; }
+.emoji-dancer_tone1 { background-position: -160px -360px; }
+.emoji-dancer_tone2 { background-position: -180px -360px; }
+.emoji-dancer_tone3 { background-position: -200px -360px; }
+.emoji-dancer_tone4 { background-position: -220px -360px; }
+.emoji-dancer_tone5 { background-position: -240px -360px; }
+.emoji-dancers { background-position: -260px -360px; }
+.emoji-dango { background-position: -280px -360px; }
+.emoji-dark_sunglasses { background-position: -300px -360px; }
+.emoji-dart { background-position: -320px -360px; }
+.emoji-dash { background-position: -340px -360px; }
+.emoji-date { background-position: -360px -360px; }
+.emoji-deciduous_tree { background-position: -380px 0; }
+.emoji-deer { background-position: -380px -20px; }
+.emoji-department_store { background-position: -380px -40px; }
+.emoji-desert { background-position: -380px -60px; }
+.emoji-desktop { background-position: -380px -80px; }
+.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; }
+.emoji-diamonds { background-position: -380px -120px; }
+.emoji-disappointed { background-position: -380px -140px; }
+.emoji-disappointed_relieved { background-position: -380px -160px; }
+.emoji-dividers { background-position: -380px -180px; }
+.emoji-dizzy { background-position: -380px -200px; }
+.emoji-dizzy_face { background-position: -380px -220px; }
+.emoji-do_not_litter { background-position: -380px -240px; }
+.emoji-dog { background-position: -380px -260px; }
+.emoji-dog2 { background-position: -380px -280px; }
+.emoji-dollar { background-position: -380px -300px; }
+.emoji-dolls { background-position: -380px -320px; }
+.emoji-dolphin { background-position: -380px -340px; }
+.emoji-door { background-position: -380px -360px; }
+.emoji-doughnut { background-position: 0 -380px; }
+.emoji-dove { background-position: -20px -380px; }
+.emoji-dragon { background-position: -40px -380px; }
+.emoji-dragon_face { background-position: -60px -380px; }
+.emoji-dress { background-position: -80px -380px; }
+.emoji-dromedary_camel { background-position: -100px -380px; }
+.emoji-drooling_face { background-position: -120px -380px; }
+.emoji-droplet { background-position: -140px -380px; }
+.emoji-drum { background-position: -160px -380px; }
+.emoji-duck { background-position: -180px -380px; }
+.emoji-dvd { background-position: -200px -380px; }
+.emoji-e-mail { background-position: -220px -380px; }
+.emoji-eagle { background-position: -240px -380px; }
+.emoji-ear { background-position: -260px -380px; }
+.emoji-ear_of_rice { background-position: -280px -380px; }
+.emoji-ear_tone1 { background-position: -300px -380px; }
+.emoji-ear_tone2 { background-position: -320px -380px; }
+.emoji-ear_tone3 { background-position: -340px -380px; }
+.emoji-ear_tone4 { background-position: -360px -380px; }
+.emoji-ear_tone5 { background-position: -380px -380px; }
+.emoji-earth_africa { background-position: -400px 0; }
+.emoji-earth_americas { background-position: -400px -20px; }
+.emoji-earth_asia { background-position: -400px -40px; }
+.emoji-egg { background-position: -400px -60px; }
+.emoji-eggplant { background-position: -400px -80px; }
+.emoji-eight { background-position: -400px -100px; }
+.emoji-eight_pointed_black_star { background-position: -400px -120px; }
+.emoji-eight_spoked_asterisk { background-position: -400px -140px; }
+.emoji-eject { background-position: -400px -160px; }
+.emoji-electric_plug { background-position: -400px -180px; }
+.emoji-elephant { background-position: -400px -200px; }
+.emoji-end { background-position: -400px -220px; }
+.emoji-envelope { background-position: -400px -240px; }
+.emoji-envelope_with_arrow { background-position: -400px -260px; }
+.emoji-euro { background-position: -400px -280px; }
+.emoji-european_castle { background-position: -400px -300px; }
+.emoji-european_post_office { background-position: -400px -320px; }
+.emoji-evergreen_tree { background-position: -400px -340px; }
+.emoji-exclamation { background-position: -400px -360px; }
+.emoji-expressionless { background-position: -400px -380px; }
+.emoji-eye { background-position: 0 -400px; }
+.emoji-eye_in_speech_bubble { background-position: -20px -400px; }
+.emoji-eyeglasses { background-position: -40px -400px; }
+.emoji-eyes { background-position: -60px -400px; }
+.emoji-face_palm { background-position: -80px -400px; }
+.emoji-face_palm_tone1 { background-position: -100px -400px; }
+.emoji-face_palm_tone2 { background-position: -120px -400px; }
+.emoji-face_palm_tone3 { background-position: -140px -400px; }
+.emoji-face_palm_tone4 { background-position: -160px -400px; }
+.emoji-face_palm_tone5 { background-position: -180px -400px; }
+.emoji-factory { background-position: -200px -400px; }
+.emoji-fallen_leaf { background-position: -220px -400px; }
+.emoji-family { background-position: -240px -400px; }
+.emoji-family_mmb { background-position: -260px -400px; }
+.emoji-family_mmbb { background-position: -280px -400px; }
+.emoji-family_mmg { background-position: -300px -400px; }
+.emoji-family_mmgb { background-position: -320px -400px; }
+.emoji-family_mmgg { background-position: -340px -400px; }
+.emoji-family_mwbb { background-position: -360px -400px; }
+.emoji-family_mwg { background-position: -380px -400px; }
+.emoji-family_mwgb { background-position: -400px -400px; }
+.emoji-family_mwgg { background-position: -420px 0; }
+.emoji-family_wwb { background-position: -420px -20px; }
+.emoji-family_wwbb { background-position: -420px -40px; }
+.emoji-family_wwg { background-position: -420px -60px; }
+.emoji-family_wwgb { background-position: -420px -80px; }
+.emoji-family_wwgg { background-position: -420px -100px; }
+.emoji-fast_forward { background-position: -420px -120px; }
+.emoji-fax { background-position: -420px -140px; }
+.emoji-fearful { background-position: -420px -160px; }
+.emoji-feet { background-position: -420px -180px; }
+.emoji-fencer { background-position: -420px -200px; }
+.emoji-ferris_wheel { background-position: -420px -220px; }
+.emoji-ferry { background-position: -420px -240px; }
+.emoji-field_hockey { background-position: -420px -260px; }
+.emoji-file_cabinet { background-position: -420px -280px; }
+.emoji-file_folder { background-position: -420px -300px; }
+.emoji-film_frames { background-position: -420px -320px; }
+.emoji-fingers_crossed { background-position: -420px -340px; }
+.emoji-fingers_crossed_tone1 { background-position: -420px -360px; }
+.emoji-fingers_crossed_tone2 { background-position: -420px -380px; }
+.emoji-fingers_crossed_tone3 { background-position: -420px -400px; }
+.emoji-fingers_crossed_tone4 { background-position: 0 -420px; }
+.emoji-fingers_crossed_tone5 { background-position: -20px -420px; }
+.emoji-fire { background-position: -40px -420px; }
+.emoji-fire_engine { background-position: -60px -420px; }
+.emoji-fireworks { background-position: -80px -420px; }
+.emoji-first_place { background-position: -100px -420px; }
+.emoji-first_quarter_moon { background-position: -120px -420px; }
+.emoji-first_quarter_moon_with_face { background-position: -140px -420px; }
+.emoji-fish { background-position: -160px -420px; }
+.emoji-fish_cake { background-position: -180px -420px; }
+.emoji-fishing_pole_and_fish { background-position: -200px -420px; }
+.emoji-fist { background-position: -220px -420px; }
+.emoji-fist_tone1 { background-position: -240px -420px; }
+.emoji-fist_tone2 { background-position: -260px -420px; }
+.emoji-fist_tone3 { background-position: -280px -420px; }
+.emoji-fist_tone4 { background-position: -300px -420px; }
+.emoji-fist_tone5 { background-position: -320px -420px; }
+.emoji-five { background-position: -340px -420px; }
+.emoji-flag_ac { background-position: -360px -420px; }
+.emoji-flag_ad { background-position: -380px -420px; }
+.emoji-flag_ae { background-position: -400px -420px; }
+.emoji-flag_af { background-position: -420px -420px; }
+.emoji-flag_ag { background-position: -440px 0; }
+.emoji-flag_ai { background-position: -440px -20px; }
+.emoji-flag_al { background-position: -440px -40px; }
+.emoji-flag_am { background-position: -440px -60px; }
+.emoji-flag_ao { background-position: -440px -80px; }
+.emoji-flag_aq { background-position: -440px -100px; }
+.emoji-flag_ar { background-position: -440px -120px; }
+.emoji-flag_as { background-position: -440px -140px; }
+.emoji-flag_at { background-position: -440px -160px; }
+.emoji-flag_au { background-position: -440px -180px; }
+.emoji-flag_aw { background-position: -440px -200px; }
+.emoji-flag_ax { background-position: -440px -220px; }
+.emoji-flag_az { background-position: -440px -240px; }
+.emoji-flag_ba { background-position: -440px -260px; }
+.emoji-flag_bb { background-position: -440px -280px; }
+.emoji-flag_bd { background-position: -440px -300px; }
+.emoji-flag_be { background-position: -440px -320px; }
+.emoji-flag_bf { background-position: -440px -340px; }
+.emoji-flag_bg { background-position: -440px -360px; }
+.emoji-flag_bh { background-position: -440px -380px; }
+.emoji-flag_bi { background-position: -440px -400px; }
+.emoji-flag_bj { background-position: -440px -420px; }
+.emoji-flag_bl { background-position: 0 -440px; }
+.emoji-flag_black { background-position: -20px -440px; }
+.emoji-flag_bm { background-position: -40px -440px; }
+.emoji-flag_bn { background-position: -60px -440px; }
+.emoji-flag_bo { background-position: -80px -440px; }
+.emoji-flag_bq { background-position: -100px -440px; }
+.emoji-flag_br { background-position: -120px -440px; }
+.emoji-flag_bs { background-position: -140px -440px; }
+.emoji-flag_bt { background-position: -160px -440px; }
+.emoji-flag_bv { background-position: -180px -440px; }
+.emoji-flag_bw { background-position: -200px -440px; }
+.emoji-flag_by { background-position: -220px -440px; }
+.emoji-flag_bz { background-position: -240px -440px; }
+.emoji-flag_ca { background-position: -260px -440px; }
+.emoji-flag_cc { background-position: -280px -440px; }
+.emoji-flag_cd { background-position: -300px -440px; }
+.emoji-flag_cf { background-position: -320px -440px; }
+.emoji-flag_cg { background-position: -340px -440px; }
+.emoji-flag_ch { background-position: -360px -440px; }
+.emoji-flag_ci { background-position: -380px -440px; }
+.emoji-flag_ck { background-position: -400px -440px; }
+.emoji-flag_cl { background-position: -420px -440px; }
+.emoji-flag_cm { background-position: -440px -440px; }
+.emoji-flag_cn { background-position: -460px 0; }
+.emoji-flag_co { background-position: -460px -20px; }
+.emoji-flag_cp { background-position: -460px -40px; }
+.emoji-flag_cr { background-position: -460px -60px; }
+.emoji-flag_cu { background-position: -460px -80px; }
+.emoji-flag_cv { background-position: -460px -100px; }
+.emoji-flag_cw { background-position: -460px -120px; }
+.emoji-flag_cx { background-position: -460px -140px; }
+.emoji-flag_cy { background-position: -460px -160px; }
+.emoji-flag_cz { background-position: -460px -180px; }
+.emoji-flag_de { background-position: -460px -200px; }
+.emoji-flag_dg { background-position: -460px -220px; }
+.emoji-flag_dj { background-position: -460px -240px; }
+.emoji-flag_dk { background-position: -460px -260px; }
+.emoji-flag_dm { background-position: -460px -280px; }
+.emoji-flag_do { background-position: -460px -300px; }
+.emoji-flag_dz { background-position: -460px -320px; }
+.emoji-flag_ea { background-position: -460px -340px; }
+.emoji-flag_ec { background-position: -460px -360px; }
+.emoji-flag_ee { background-position: -460px -380px; }
+.emoji-flag_eg { background-position: -460px -400px; }
+.emoji-flag_eh { background-position: -460px -420px; }
+.emoji-flag_er { background-position: -460px -440px; }
+.emoji-flag_es { background-position: 0 -460px; }
+.emoji-flag_et { background-position: -20px -460px; }
+.emoji-flag_eu { background-position: -40px -460px; }
+.emoji-flag_fi { background-position: -60px -460px; }
+.emoji-flag_fj { background-position: -80px -460px; }
+.emoji-flag_fk { background-position: -100px -460px; }
+.emoji-flag_fm { background-position: -120px -460px; }
+.emoji-flag_fo { background-position: -140px -460px; }
+.emoji-flag_fr { background-position: -160px -460px; }
+.emoji-flag_ga { background-position: -180px -460px; }
+.emoji-flag_gb { background-position: -200px -460px; }
+.emoji-flag_gd { background-position: -220px -460px; }
+.emoji-flag_ge { background-position: -240px -460px; }
+.emoji-flag_gf { background-position: -260px -460px; }
+.emoji-flag_gg { background-position: -280px -460px; }
+.emoji-flag_gh { background-position: -300px -460px; }
+.emoji-flag_gi { background-position: -320px -460px; }
+.emoji-flag_gl { background-position: -340px -460px; }
+.emoji-flag_gm { background-position: -360px -460px; }
+.emoji-flag_gn { background-position: -380px -460px; }
+.emoji-flag_gp { background-position: -400px -460px; }
+.emoji-flag_gq { background-position: -420px -460px; }
+.emoji-flag_gr { background-position: -440px -460px; }
+.emoji-flag_gs { background-position: -460px -460px; }
+.emoji-flag_gt { background-position: -480px 0; }
+.emoji-flag_gu { background-position: -480px -20px; }
+.emoji-flag_gw { background-position: -480px -40px; }
+.emoji-flag_gy { background-position: -480px -60px; }
+.emoji-flag_hk { background-position: -480px -80px; }
+.emoji-flag_hm { background-position: -480px -100px; }
+.emoji-flag_hn { background-position: -480px -120px; }
+.emoji-flag_hr { background-position: -480px -140px; }
+.emoji-flag_ht { background-position: -480px -160px; }
+.emoji-flag_hu { background-position: -480px -180px; }
+.emoji-flag_ic { background-position: -480px -200px; }
+.emoji-flag_id { background-position: -480px -220px; }
+.emoji-flag_ie { background-position: -480px -240px; }
+.emoji-flag_il { background-position: -480px -260px; }
+.emoji-flag_im { background-position: -480px -280px; }
+.emoji-flag_in { background-position: -480px -300px; }
+.emoji-flag_io { background-position: -480px -320px; }
+.emoji-flag_iq { background-position: -480px -340px; }
+.emoji-flag_ir { background-position: -480px -360px; }
+.emoji-flag_is { background-position: -480px -380px; }
+.emoji-flag_it { background-position: -480px -400px; }
+.emoji-flag_je { background-position: -480px -420px; }
+.emoji-flag_jm { background-position: -480px -440px; }
+.emoji-flag_jo { background-position: -480px -460px; }
+.emoji-flag_jp { background-position: 0 -480px; }
+.emoji-flag_ke { background-position: -20px -480px; }
+.emoji-flag_kg { background-position: -40px -480px; }
+.emoji-flag_kh { background-position: -60px -480px; }
+.emoji-flag_ki { background-position: -80px -480px; }
+.emoji-flag_km { background-position: -100px -480px; }
+.emoji-flag_kn { background-position: -120px -480px; }
+.emoji-flag_kp { background-position: -140px -480px; }
+.emoji-flag_kr { background-position: -160px -480px; }
+.emoji-flag_kw { background-position: -180px -480px; }
+.emoji-flag_ky { background-position: -200px -480px; }
+.emoji-flag_kz { background-position: -220px -480px; }
+.emoji-flag_la { background-position: -240px -480px; }
+.emoji-flag_lb { background-position: -260px -480px; }
+.emoji-flag_lc { background-position: -280px -480px; }
+.emoji-flag_li { background-position: -300px -480px; }
+.emoji-flag_lk { background-position: -320px -480px; }
+.emoji-flag_lr { background-position: -340px -480px; }
+.emoji-flag_ls { background-position: -360px -480px; }
+.emoji-flag_lt { background-position: -380px -480px; }
+.emoji-flag_lu { background-position: -400px -480px; }
+.emoji-flag_lv { background-position: -420px -480px; }
+.emoji-flag_ly { background-position: -440px -480px; }
+.emoji-flag_ma { background-position: -460px -480px; }
+.emoji-flag_mc { background-position: -480px -480px; }
+.emoji-flag_md { background-position: -500px 0; }
+.emoji-flag_me { background-position: -500px -20px; }
+.emoji-flag_mf { background-position: -500px -40px; }
+.emoji-flag_mg { background-position: -500px -60px; }
+.emoji-flag_mh { background-position: -500px -80px; }
+.emoji-flag_mk { background-position: -500px -100px; }
+.emoji-flag_ml { background-position: -500px -120px; }
+.emoji-flag_mm { background-position: -500px -140px; }
+.emoji-flag_mn { background-position: -500px -160px; }
+.emoji-flag_mo { background-position: -500px -180px; }
+.emoji-flag_mp { background-position: -500px -200px; }
+.emoji-flag_mq { background-position: -500px -220px; }
+.emoji-flag_mr { background-position: -500px -240px; }
+.emoji-flag_ms { background-position: -500px -260px; }
+.emoji-flag_mt { background-position: -500px -280px; }
+.emoji-flag_mu { background-position: -500px -300px; }
+.emoji-flag_mv { background-position: -500px -320px; }
+.emoji-flag_mw { background-position: -500px -340px; }
+.emoji-flag_mx { background-position: -500px -360px; }
+.emoji-flag_my { background-position: -500px -380px; }
+.emoji-flag_mz { background-position: -500px -400px; }
+.emoji-flag_na { background-position: -500px -420px; }
+.emoji-flag_nc { background-position: -500px -440px; }
+.emoji-flag_ne { background-position: -500px -460px; }
+.emoji-flag_nf { background-position: -500px -480px; }
+.emoji-flag_ng { background-position: 0 -500px; }
+.emoji-flag_ni { background-position: -20px -500px; }
+.emoji-flag_nl { background-position: -40px -500px; }
+.emoji-flag_no { background-position: -60px -500px; }
+.emoji-flag_np { background-position: -80px -500px; }
+.emoji-flag_nr { background-position: -100px -500px; }
+.emoji-flag_nu { background-position: -120px -500px; }
+.emoji-flag_nz { background-position: -140px -500px; }
+.emoji-flag_om { background-position: -160px -500px; }
+.emoji-flag_pa { background-position: -180px -500px; }
+.emoji-flag_pe { background-position: -200px -500px; }
+.emoji-flag_pf { background-position: -220px -500px; }
+.emoji-flag_pg { background-position: -240px -500px; }
+.emoji-flag_ph { background-position: -260px -500px; }
+.emoji-flag_pk { background-position: -280px -500px; }
+.emoji-flag_pl { background-position: -300px -500px; }
+.emoji-flag_pm { background-position: -320px -500px; }
+.emoji-flag_pn { background-position: -340px -500px; }
+.emoji-flag_pr { background-position: -360px -500px; }
+.emoji-flag_ps { background-position: -380px -500px; }
+.emoji-flag_pt { background-position: -400px -500px; }
+.emoji-flag_pw { background-position: -420px -500px; }
+.emoji-flag_py { background-position: -440px -500px; }
+.emoji-flag_qa { background-position: -460px -500px; }
+.emoji-flag_re { background-position: -480px -500px; }
+.emoji-flag_ro { background-position: -500px -500px; }
+.emoji-flag_rs { background-position: -520px 0; }
+.emoji-flag_ru { background-position: -520px -20px; }
+.emoji-flag_rw { background-position: -520px -40px; }
+.emoji-flag_sa { background-position: -520px -60px; }
+.emoji-flag_sb { background-position: -520px -80px; }
+.emoji-flag_sc { background-position: -520px -100px; }
+.emoji-flag_sd { background-position: -520px -120px; }
+.emoji-flag_se { background-position: -520px -140px; }
+.emoji-flag_sg { background-position: -520px -160px; }
+.emoji-flag_sh { background-position: -520px -180px; }
+.emoji-flag_si { background-position: -520px -200px; }
+.emoji-flag_sj { background-position: -520px -220px; }
+.emoji-flag_sk { background-position: -520px -240px; }
+.emoji-flag_sl { background-position: -520px -260px; }
+.emoji-flag_sm { background-position: -520px -280px; }
+.emoji-flag_sn { background-position: -520px -300px; }
+.emoji-flag_so { background-position: -520px -320px; }
+.emoji-flag_sr { background-position: -520px -340px; }
+.emoji-flag_ss { background-position: -520px -360px; }
+.emoji-flag_st { background-position: -520px -380px; }
+.emoji-flag_sv { background-position: -520px -400px; }
+.emoji-flag_sx { background-position: -520px -420px; }
+.emoji-flag_sy { background-position: -520px -440px; }
+.emoji-flag_sz { background-position: -520px -460px; }
+.emoji-flag_ta { background-position: -520px -480px; }
+.emoji-flag_tc { background-position: -520px -500px; }
+.emoji-flag_td { background-position: 0 -520px; }
+.emoji-flag_tf { background-position: -20px -520px; }
+.emoji-flag_tg { background-position: -40px -520px; }
+.emoji-flag_th { background-position: -60px -520px; }
+.emoji-flag_tj { background-position: -80px -520px; }
+.emoji-flag_tk { background-position: -100px -520px; }
+.emoji-flag_tl { background-position: -120px -520px; }
+.emoji-flag_tm { background-position: -140px -520px; }
+.emoji-flag_tn { background-position: -160px -520px; }
+.emoji-flag_to { background-position: -180px -520px; }
+.emoji-flag_tr { background-position: -200px -520px; }
+.emoji-flag_tt { background-position: -220px -520px; }
+.emoji-flag_tv { background-position: -240px -520px; }
+.emoji-flag_tw { background-position: -260px -520px; }
+.emoji-flag_tz { background-position: -280px -520px; }
+.emoji-flag_ua { background-position: -300px -520px; }
+.emoji-flag_ug { background-position: -320px -520px; }
+.emoji-flag_um { background-position: -340px -520px; }
+.emoji-flag_us { background-position: -360px -520px; }
+.emoji-flag_uy { background-position: -380px -520px; }
+.emoji-flag_uz { background-position: -400px -520px; }
+.emoji-flag_va { background-position: -420px -520px; }
+.emoji-flag_vc { background-position: -440px -520px; }
+.emoji-flag_ve { background-position: -460px -520px; }
+.emoji-flag_vg { background-position: -480px -520px; }
+.emoji-flag_vi { background-position: -500px -520px; }
+.emoji-flag_vn { background-position: -520px -520px; }
+.emoji-flag_vu { background-position: -540px 0; }
+.emoji-flag_wf { background-position: -540px -20px; }
+.emoji-flag_white { background-position: -540px -40px; }
+.emoji-flag_ws { background-position: -540px -60px; }
+.emoji-flag_xk { background-position: -540px -80px; }
+.emoji-flag_ye { background-position: -540px -100px; }
+.emoji-flag_yt { background-position: -540px -120px; }
+.emoji-flag_za { background-position: -540px -140px; }
+.emoji-flag_zm { background-position: -540px -160px; }
+.emoji-flag_zw { background-position: -540px -180px; }
+.emoji-flags { background-position: -540px -200px; }
+.emoji-flashlight { background-position: -540px -220px; }
+.emoji-fleur-de-lis { background-position: -540px -240px; }
+.emoji-floppy_disk { background-position: -540px -260px; }
+.emoji-flower_playing_cards { background-position: -540px -280px; }
+.emoji-flushed { background-position: -540px -300px; }
+.emoji-fog { background-position: -540px -320px; }
+.emoji-foggy { background-position: -540px -340px; }
+.emoji-football { background-position: -540px -360px; }
+.emoji-footprints { background-position: -540px -380px; }
+.emoji-fork_and_knife { background-position: -540px -400px; }
+.emoji-fork_knife_plate { background-position: -540px -420px; }
+.emoji-fountain { background-position: -540px -440px; }
+.emoji-four { background-position: -540px -460px; }
+.emoji-four_leaf_clover { background-position: -540px -480px; }
+.emoji-fox { background-position: -540px -500px; }
+.emoji-frame_photo { background-position: -540px -520px; }
+.emoji-free { background-position: 0 -540px; }
+.emoji-french_bread { background-position: -20px -540px; }
+.emoji-fried_shrimp { background-position: -40px -540px; }
+.emoji-fries { background-position: -60px -540px; }
+.emoji-frog { background-position: -80px -540px; }
+.emoji-frowning { background-position: -100px -540px; }
+.emoji-frowning2 { background-position: -120px -540px; }
+.emoji-fuelpump { background-position: -140px -540px; }
+.emoji-full_moon { background-position: -160px -540px; }
+.emoji-full_moon_with_face { background-position: -180px -540px; }
+.emoji-game_die { background-position: -200px -540px; }
+.emoji-gear { background-position: -220px -540px; }
+.emoji-gem { background-position: -240px -540px; }
+.emoji-gemini { background-position: -260px -540px; }
+.emoji-ghost { background-position: -280px -540px; }
+.emoji-gift { background-position: -300px -540px; }
+.emoji-gift_heart { background-position: -320px -540px; }
+.emoji-girl { background-position: -340px -540px; }
+.emoji-girl_tone1 { background-position: -360px -540px; }
+.emoji-girl_tone2 { background-position: -380px -540px; }
+.emoji-girl_tone3 { background-position: -400px -540px; }
+.emoji-girl_tone4 { background-position: -420px -540px; }
+.emoji-girl_tone5 { background-position: -440px -540px; }
+.emoji-globe_with_meridians { background-position: -460px -540px; }
+.emoji-goal { background-position: -480px -540px; }
+.emoji-goat { background-position: -500px -540px; }
+.emoji-golf { background-position: -520px -540px; }
+.emoji-golfer { background-position: -540px -540px; }
+.emoji-gorilla { background-position: -560px 0; }
+.emoji-grapes { background-position: -560px -20px; }
+.emoji-green_apple { background-position: -560px -40px; }
+.emoji-green_book { background-position: -560px -60px; }
+.emoji-green_heart { background-position: -560px -80px; }
+.emoji-grey_exclamation { background-position: -560px -100px; }
+.emoji-grey_question { background-position: -560px -120px; }
+.emoji-grimacing { background-position: -560px -140px; }
+.emoji-grin { background-position: -560px -160px; }
+.emoji-grinning { background-position: -560px -180px; }
+.emoji-guardsman { background-position: -560px -200px; }
+.emoji-guardsman_tone1 { background-position: -560px -220px; }
+.emoji-guardsman_tone2 { background-position: -560px -240px; }
+.emoji-guardsman_tone3 { background-position: -560px -260px; }
+.emoji-guardsman_tone4 { background-position: -560px -280px; }
+.emoji-guardsman_tone5 { background-position: -560px -300px; }
+.emoji-guitar { background-position: -560px -320px; }
+.emoji-gun { background-position: -560px -340px; }
+.emoji-haircut { background-position: -560px -360px; }
+.emoji-haircut_tone1 { background-position: -560px -380px; }
+.emoji-haircut_tone2 { background-position: -560px -400px; }
+.emoji-haircut_tone3 { background-position: -560px -420px; }
+.emoji-haircut_tone4 { background-position: -560px -440px; }
+.emoji-haircut_tone5 { background-position: -560px -460px; }
+.emoji-hamburger { background-position: -560px -480px; }
+.emoji-hammer { background-position: -560px -500px; }
+.emoji-hammer_pick { background-position: -560px -520px; }
+.emoji-hamster { background-position: -560px -540px; }
+.emoji-hand_splayed { background-position: 0 -560px; }
+.emoji-hand_splayed_tone1 { background-position: -20px -560px; }
+.emoji-hand_splayed_tone2 { background-position: -40px -560px; }
+.emoji-hand_splayed_tone3 { background-position: -60px -560px; }
+.emoji-hand_splayed_tone4 { background-position: -80px -560px; }
+.emoji-hand_splayed_tone5 { background-position: -100px -560px; }
+.emoji-handbag { background-position: -120px -560px; }
+.emoji-handball { background-position: -140px -560px; }
+.emoji-handball_tone1 { background-position: -160px -560px; }
+.emoji-handball_tone2 { background-position: -180px -560px; }
+.emoji-handball_tone3 { background-position: -200px -560px; }
+.emoji-handball_tone4 { background-position: -220px -560px; }
+.emoji-handball_tone5 { background-position: -240px -560px; }
+.emoji-handshake { background-position: -260px -560px; }
+.emoji-handshake_tone1 { background-position: -280px -560px; }
+.emoji-handshake_tone2 { background-position: -300px -560px; }
+.emoji-handshake_tone3 { background-position: -320px -560px; }
+.emoji-handshake_tone4 { background-position: -340px -560px; }
+.emoji-handshake_tone5 { background-position: -360px -560px; }
+.emoji-hash { background-position: -380px -560px; }
+.emoji-hatched_chick { background-position: -400px -560px; }
+.emoji-hatching_chick { background-position: -420px -560px; }
+.emoji-head_bandage { background-position: -440px -560px; }
+.emoji-headphones { background-position: -460px -560px; }
+.emoji-hear_no_evil { background-position: -480px -560px; }
+.emoji-heart { background-position: -500px -560px; }
+.emoji-heart_decoration { background-position: -520px -560px; }
+.emoji-heart_exclamation { background-position: -540px -560px; }
+.emoji-heart_eyes { background-position: -560px -560px; }
+.emoji-heart_eyes_cat { background-position: -580px 0; }
+.emoji-heartbeat { background-position: -580px -20px; }
+.emoji-heartpulse { background-position: -580px -40px; }
+.emoji-hearts { background-position: -580px -60px; }
+.emoji-heavy_check_mark { background-position: -580px -80px; }
+.emoji-heavy_division_sign { background-position: -580px -100px; }
+.emoji-heavy_dollar_sign { background-position: -580px -120px; }
+.emoji-heavy_minus_sign { background-position: -580px -140px; }
+.emoji-heavy_multiplication_x { background-position: -580px -160px; }
+.emoji-heavy_plus_sign { background-position: -580px -180px; }
+.emoji-helicopter { background-position: -580px -200px; }
+.emoji-helmet_with_cross { background-position: -580px -220px; }
+.emoji-herb { background-position: -580px -240px; }
+.emoji-hibiscus { background-position: -580px -260px; }
+.emoji-high_brightness { background-position: -580px -280px; }
+.emoji-high_heel { background-position: -580px -300px; }
+.emoji-hockey { background-position: -580px -320px; }
+.emoji-hole { background-position: -580px -340px; }
+.emoji-homes { background-position: -580px -360px; }
+.emoji-honey_pot { background-position: -580px -380px; }
+.emoji-horse { background-position: -580px -400px; }
+.emoji-horse_racing { background-position: -580px -420px; }
+.emoji-horse_racing_tone1 { background-position: -580px -440px; }
+.emoji-horse_racing_tone2 { background-position: -580px -460px; }
+.emoji-horse_racing_tone3 { background-position: -580px -480px; }
+.emoji-horse_racing_tone4 { background-position: -580px -500px; }
+.emoji-horse_racing_tone5 { background-position: -580px -520px; }
+.emoji-hospital { background-position: -580px -540px; }
+.emoji-hot_pepper { background-position: -580px -560px; }
+.emoji-hotdog { background-position: 0 -580px; }
+.emoji-hotel { background-position: -20px -580px; }
+.emoji-hotsprings { background-position: -40px -580px; }
+.emoji-hourglass { background-position: -60px -580px; }
+.emoji-hourglass_flowing_sand { background-position: -80px -580px; }
+.emoji-house { background-position: -100px -580px; }
+.emoji-house_abandoned { background-position: -120px -580px; }
+.emoji-house_with_garden { background-position: -140px -580px; }
+.emoji-hugging { background-position: -160px -580px; }
+.emoji-hushed { background-position: -180px -580px; }
+.emoji-ice_cream { background-position: -200px -580px; }
+.emoji-ice_skate { background-position: -220px -580px; }
+.emoji-icecream { background-position: -240px -580px; }
+.emoji-id { background-position: -260px -580px; }
+.emoji-ideograph_advantage { background-position: -280px -580px; }
+.emoji-imp { background-position: -300px -580px; }
+.emoji-inbox_tray { background-position: -320px -580px; }
+.emoji-incoming_envelope { background-position: -340px -580px; }
+.emoji-information_desk_person { background-position: -360px -580px; }
+.emoji-information_desk_person_tone1 { background-position: -380px -580px; }
+.emoji-information_desk_person_tone2 { background-position: -400px -580px; }
+.emoji-information_desk_person_tone3 { background-position: -420px -580px; }
+.emoji-information_desk_person_tone4 { background-position: -440px -580px; }
+.emoji-information_desk_person_tone5 { background-position: -460px -580px; }
+.emoji-information_source { background-position: -480px -580px; }
+.emoji-innocent { background-position: -500px -580px; }
+.emoji-interrobang { background-position: -520px -580px; }
+.emoji-iphone { background-position: -540px -580px; }
+.emoji-island { background-position: -560px -580px; }
+.emoji-izakaya_lantern { background-position: -580px -580px; }
+.emoji-jack_o_lantern { background-position: -600px 0; }
+.emoji-japan { background-position: -600px -20px; }
+.emoji-japanese_castle { background-position: -600px -40px; }
+.emoji-japanese_goblin { background-position: -600px -60px; }
+.emoji-japanese_ogre { background-position: -600px -80px; }
+.emoji-jeans { background-position: -600px -100px; }
+.emoji-joy { background-position: -600px -120px; }
+.emoji-joy_cat { background-position: -600px -140px; }
+.emoji-joystick { background-position: -600px -160px; }
+.emoji-juggling { background-position: -600px -180px; }
+.emoji-juggling_tone1 { background-position: -600px -200px; }
+.emoji-juggling_tone2 { background-position: -600px -220px; }
+.emoji-juggling_tone3 { background-position: -600px -240px; }
+.emoji-juggling_tone4 { background-position: -600px -260px; }
+.emoji-juggling_tone5 { background-position: -600px -280px; }
+.emoji-kaaba { background-position: -600px -300px; }
+.emoji-key { background-position: -600px -320px; }
+.emoji-key2 { background-position: -600px -340px; }
+.emoji-keyboard { background-position: -600px -360px; }
+.emoji-kimono { background-position: -600px -380px; }
+.emoji-kiss { background-position: -600px -400px; }
+.emoji-kiss_mm { background-position: -600px -420px; }
+.emoji-kiss_ww { background-position: -600px -440px; }
+.emoji-kissing { background-position: -600px -460px; }
+.emoji-kissing_cat { background-position: -600px -480px; }
+.emoji-kissing_closed_eyes { background-position: -600px -500px; }
+.emoji-kissing_heart { background-position: -600px -520px; }
+.emoji-kissing_smiling_eyes { background-position: -600px -540px; }
+.emoji-kiwi { background-position: -600px -560px; }
+.emoji-knife { background-position: -600px -580px; }
+.emoji-koala { background-position: 0 -600px; }
+.emoji-koko { background-position: -20px -600px; }
+.emoji-label { background-position: -40px -600px; }
+.emoji-large_blue_circle { background-position: -60px -600px; }
+.emoji-large_blue_diamond { background-position: -80px -600px; }
+.emoji-large_orange_diamond { background-position: -100px -600px; }
+.emoji-last_quarter_moon { background-position: -120px -600px; }
+.emoji-last_quarter_moon_with_face { background-position: -140px -600px; }
+.emoji-laughing { background-position: -160px -600px; }
+.emoji-leaves { background-position: -180px -600px; }
+.emoji-ledger { background-position: -200px -600px; }
+.emoji-left_facing_fist { background-position: -220px -600px; }
+.emoji-left_facing_fist_tone1 { background-position: -240px -600px; }
+.emoji-left_facing_fist_tone2 { background-position: -260px -600px; }
+.emoji-left_facing_fist_tone3 { background-position: -280px -600px; }
+.emoji-left_facing_fist_tone4 { background-position: -300px -600px; }
+.emoji-left_facing_fist_tone5 { background-position: -320px -600px; }
+.emoji-left_luggage { background-position: -340px -600px; }
+.emoji-left_right_arrow { background-position: -360px -600px; }
+.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; }
+.emoji-lemon { background-position: -400px -600px; }
+.emoji-leo { background-position: -420px -600px; }
+.emoji-leopard { background-position: -440px -600px; }
+.emoji-level_slider { background-position: -460px -600px; }
+.emoji-levitate { background-position: -480px -600px; }
+.emoji-libra { background-position: -500px -600px; }
+.emoji-lifter { background-position: -520px -600px; }
+.emoji-lifter_tone1 { background-position: -540px -600px; }
+.emoji-lifter_tone2 { background-position: -560px -600px; }
+.emoji-lifter_tone3 { background-position: -580px -600px; }
+.emoji-lifter_tone4 { background-position: -600px -600px; }
+.emoji-lifter_tone5 { background-position: -620px 0; }
+.emoji-light_rail { background-position: -620px -20px; }
+.emoji-link { background-position: -620px -40px; }
+.emoji-lion_face { background-position: -620px -60px; }
+.emoji-lips { background-position: -620px -80px; }
+.emoji-lipstick { background-position: -620px -100px; }
+.emoji-lizard { background-position: -620px -120px; }
+.emoji-lock { background-position: -620px -140px; }
+.emoji-lock_with_ink_pen { background-position: -620px -160px; }
+.emoji-lollipop { background-position: -620px -180px; }
+.emoji-loop { background-position: -620px -200px; }
+.emoji-loud_sound { background-position: -620px -220px; }
+.emoji-loudspeaker { background-position: -620px -240px; }
+.emoji-love_hotel { background-position: -620px -260px; }
+.emoji-love_letter { background-position: -620px -280px; }
+.emoji-low_brightness { background-position: -620px -300px; }
+.emoji-lying_face { background-position: -620px -320px; }
+.emoji-m { background-position: -620px -340px; }
+.emoji-mag { background-position: -620px -360px; }
+.emoji-mag_right { background-position: -620px -380px; }
+.emoji-mahjong { background-position: -620px -400px; }
+.emoji-mailbox { background-position: -620px -420px; }
+.emoji-mailbox_closed { background-position: -620px -440px; }
+.emoji-mailbox_with_mail { background-position: -620px -460px; }
+.emoji-mailbox_with_no_mail { background-position: -620px -480px; }
+.emoji-man { background-position: -620px -500px; }
+.emoji-man_dancing { background-position: -620px -520px; }
+.emoji-man_dancing_tone1 { background-position: -620px -540px; }
+.emoji-man_dancing_tone2 { background-position: -620px -560px; }
+.emoji-man_dancing_tone3 { background-position: -620px -580px; }
+.emoji-man_dancing_tone4 { background-position: -620px -600px; }
+.emoji-man_dancing_tone5 { background-position: 0 -620px; }
+.emoji-man_in_tuxedo { background-position: -20px -620px; }
+.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; }
+.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; }
+.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; }
+.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; }
+.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; }
+.emoji-man_tone1 { background-position: -140px -620px; }
+.emoji-man_tone2 { background-position: -160px -620px; }
+.emoji-man_tone3 { background-position: -180px -620px; }
+.emoji-man_tone4 { background-position: -200px -620px; }
+.emoji-man_tone5 { background-position: -220px -620px; }
+.emoji-man_with_gua_pi_mao { background-position: -240px -620px; }
+.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; }
+.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; }
+.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; }
+.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; }
+.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; }
+.emoji-man_with_turban { background-position: -360px -620px; }
+.emoji-man_with_turban_tone1 { background-position: -380px -620px; }
+.emoji-man_with_turban_tone2 { background-position: -400px -620px; }
+.emoji-man_with_turban_tone3 { background-position: -420px -620px; }
+.emoji-man_with_turban_tone4 { background-position: -440px -620px; }
+.emoji-man_with_turban_tone5 { background-position: -460px -620px; }
+.emoji-mans_shoe { background-position: -480px -620px; }
+.emoji-map { background-position: -500px -620px; }
+.emoji-maple_leaf { background-position: -520px -620px; }
+.emoji-martial_arts_uniform { background-position: -540px -620px; }
+.emoji-mask { background-position: -560px -620px; }
+.emoji-massage { background-position: -580px -620px; }
+.emoji-massage_tone1 { background-position: -600px -620px; }
+.emoji-massage_tone2 { background-position: -620px -620px; }
+.emoji-massage_tone3 { background-position: -640px 0; }
+.emoji-massage_tone4 { background-position: -640px -20px; }
+.emoji-massage_tone5 { background-position: -640px -40px; }
+.emoji-meat_on_bone { background-position: -640px -60px; }
+.emoji-medal { background-position: -640px -80px; }
+.emoji-mega { background-position: -640px -100px; }
+.emoji-melon { background-position: -640px -120px; }
+.emoji-menorah { background-position: -640px -140px; }
+.emoji-mens { background-position: -640px -160px; }
+.emoji-metal { background-position: -640px -180px; }
+.emoji-metal_tone1 { background-position: -640px -200px; }
+.emoji-metal_tone2 { background-position: -640px -220px; }
+.emoji-metal_tone3 { background-position: -640px -240px; }
+.emoji-metal_tone4 { background-position: -640px -260px; }
+.emoji-metal_tone5 { background-position: -640px -280px; }
+.emoji-metro { background-position: -640px -300px; }
+.emoji-microphone { background-position: -640px -320px; }
+.emoji-microphone2 { background-position: -640px -340px; }
+.emoji-microscope { background-position: -640px -360px; }
+.emoji-middle_finger { background-position: -640px -380px; }
+.emoji-middle_finger_tone1 { background-position: -640px -400px; }
+.emoji-middle_finger_tone2 { background-position: -640px -420px; }
+.emoji-middle_finger_tone3 { background-position: -640px -440px; }
+.emoji-middle_finger_tone4 { background-position: -640px -460px; }
+.emoji-middle_finger_tone5 { background-position: -640px -480px; }
+.emoji-military_medal { background-position: -640px -500px; }
+.emoji-milk { background-position: -640px -520px; }
+.emoji-milky_way { background-position: -640px -540px; }
+.emoji-minibus { background-position: -640px -560px; }
+.emoji-minidisc { background-position: -640px -580px; }
+.emoji-mobile_phone_off { background-position: -640px -600px; }
+.emoji-money_mouth { background-position: -640px -620px; }
+.emoji-money_with_wings { background-position: 0 -640px; }
+.emoji-moneybag { background-position: -20px -640px; }
+.emoji-monkey { background-position: -40px -640px; }
+.emoji-monkey_face { background-position: -60px -640px; }
+.emoji-monorail { background-position: -80px -640px; }
+.emoji-mortar_board { background-position: -100px -640px; }
+.emoji-mosque { background-position: -120px -640px; }
+.emoji-motor_scooter { background-position: -140px -640px; }
+.emoji-motorboat { background-position: -160px -640px; }
+.emoji-motorcycle { background-position: -180px -640px; }
+.emoji-motorway { background-position: -200px -640px; }
+.emoji-mount_fuji { background-position: -220px -640px; }
+.emoji-mountain { background-position: -240px -640px; }
+.emoji-mountain_bicyclist { background-position: -260px -640px; }
+.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; }
+.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; }
+.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; }
+.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; }
+.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; }
+.emoji-mountain_cableway { background-position: -380px -640px; }
+.emoji-mountain_railway { background-position: -400px -640px; }
+.emoji-mountain_snow { background-position: -420px -640px; }
+.emoji-mouse { background-position: -440px -640px; }
+.emoji-mouse2 { background-position: -460px -640px; }
+.emoji-mouse_three_button { background-position: -480px -640px; }
+.emoji-movie_camera { background-position: -500px -640px; }
+.emoji-moyai { background-position: -520px -640px; }
+.emoji-mrs_claus { background-position: -540px -640px; }
+.emoji-mrs_claus_tone1 { background-position: -560px -640px; }
+.emoji-mrs_claus_tone2 { background-position: -580px -640px; }
+.emoji-mrs_claus_tone3 { background-position: -600px -640px; }
+.emoji-mrs_claus_tone4 { background-position: -620px -640px; }
+.emoji-mrs_claus_tone5 { background-position: -640px -640px; }
+.emoji-muscle { background-position: -660px 0; }
+.emoji-muscle_tone1 { background-position: -660px -20px; }
+.emoji-muscle_tone2 { background-position: -660px -40px; }
+.emoji-muscle_tone3 { background-position: -660px -60px; }
+.emoji-muscle_tone4 { background-position: -660px -80px; }
+.emoji-muscle_tone5 { background-position: -660px -100px; }
+.emoji-mushroom { background-position: -660px -120px; }
+.emoji-musical_keyboard { background-position: -660px -140px; }
+.emoji-musical_note { background-position: -660px -160px; }
+.emoji-musical_score { background-position: -660px -180px; }
+.emoji-mute { background-position: -660px -200px; }
+.emoji-nail_care { background-position: -660px -220px; }
+.emoji-nail_care_tone1 { background-position: -660px -240px; }
+.emoji-nail_care_tone2 { background-position: -660px -260px; }
+.emoji-nail_care_tone3 { background-position: -660px -280px; }
+.emoji-nail_care_tone4 { background-position: -660px -300px; }
+.emoji-nail_care_tone5 { background-position: -660px -320px; }
+.emoji-name_badge { background-position: -660px -340px; }
+.emoji-nauseated_face { background-position: -660px -360px; }
+.emoji-necktie { background-position: -660px -380px; }
+.emoji-negative_squared_cross_mark { background-position: -660px -400px; }
+.emoji-nerd { background-position: -660px -420px; }
+.emoji-neutral_face { background-position: -660px -440px; }
+.emoji-new { background-position: -660px -460px; }
+.emoji-new_moon { background-position: -660px -480px; }
+.emoji-new_moon_with_face { background-position: -660px -500px; }
+.emoji-newspaper { background-position: -660px -520px; }
+.emoji-newspaper2 { background-position: -660px -540px; }
+.emoji-ng { background-position: -660px -560px; }
+.emoji-night_with_stars { background-position: -660px -580px; }
+.emoji-nine { background-position: -660px -600px; }
+.emoji-no_bell { background-position: -660px -620px; }
+.emoji-no_bicycles { background-position: -660px -640px; }
+.emoji-no_entry { background-position: 0 -660px; }
+.emoji-no_entry_sign { background-position: -20px -660px; }
+.emoji-no_good { background-position: -40px -660px; }
+.emoji-no_good_tone1 { background-position: -60px -660px; }
+.emoji-no_good_tone2 { background-position: -80px -660px; }
+.emoji-no_good_tone3 { background-position: -100px -660px; }
+.emoji-no_good_tone4 { background-position: -120px -660px; }
+.emoji-no_good_tone5 { background-position: -140px -660px; }
+.emoji-no_mobile_phones { background-position: -160px -660px; }
+.emoji-no_mouth { background-position: -180px -660px; }
+.emoji-no_pedestrians { background-position: -200px -660px; }
+.emoji-no_smoking { background-position: -220px -660px; }
+.emoji-non-potable_water { background-position: -240px -660px; }
+.emoji-nose { background-position: -260px -660px; }
+.emoji-nose_tone1 { background-position: -280px -660px; }
+.emoji-nose_tone2 { background-position: -300px -660px; }
+.emoji-nose_tone3 { background-position: -320px -660px; }
+.emoji-nose_tone4 { background-position: -340px -660px; }
+.emoji-nose_tone5 { background-position: -360px -660px; }
+.emoji-notebook { background-position: -380px -660px; }
+.emoji-notebook_with_decorative_cover { background-position: -400px -660px; }
+.emoji-notepad_spiral { background-position: -420px -660px; }
+.emoji-notes { background-position: -440px -660px; }
+.emoji-nut_and_bolt { background-position: -460px -660px; }
+.emoji-o { background-position: -480px -660px; }
+.emoji-o2 { background-position: -500px -660px; }
+.emoji-ocean { background-position: -520px -660px; }
+.emoji-octagonal_sign { background-position: -540px -660px; }
+.emoji-octopus { background-position: -560px -660px; }
+.emoji-oden { background-position: -580px -660px; }
+.emoji-office { background-position: -600px -660px; }
+.emoji-oil { background-position: -620px -660px; }
+.emoji-ok { background-position: -640px -660px; }
+.emoji-ok_hand { background-position: -660px -660px; }
+.emoji-ok_hand_tone1 { background-position: -680px 0; }
+.emoji-ok_hand_tone2 { background-position: -680px -20px; }
+.emoji-ok_hand_tone3 { background-position: -680px -40px; }
+.emoji-ok_hand_tone4 { background-position: -680px -60px; }
+.emoji-ok_hand_tone5 { background-position: -680px -80px; }
+.emoji-ok_woman { background-position: -680px -100px; }
+.emoji-ok_woman_tone1 { background-position: -680px -120px; }
+.emoji-ok_woman_tone2 { background-position: -680px -140px; }
+.emoji-ok_woman_tone3 { background-position: -680px -160px; }
+.emoji-ok_woman_tone4 { background-position: -680px -180px; }
+.emoji-ok_woman_tone5 { background-position: -680px -200px; }
+.emoji-older_man { background-position: -680px -220px; }
+.emoji-older_man_tone1 { background-position: -680px -240px; }
+.emoji-older_man_tone2 { background-position: -680px -260px; }
+.emoji-older_man_tone3 { background-position: -680px -280px; }
+.emoji-older_man_tone4 { background-position: -680px -300px; }
+.emoji-older_man_tone5 { background-position: -680px -320px; }
+.emoji-older_woman { background-position: -680px -340px; }
+.emoji-older_woman_tone1 { background-position: -680px -360px; }
+.emoji-older_woman_tone2 { background-position: -680px -380px; }
+.emoji-older_woman_tone3 { background-position: -680px -400px; }
+.emoji-older_woman_tone4 { background-position: -680px -420px; }
+.emoji-older_woman_tone5 { background-position: -680px -440px; }
+.emoji-om_symbol { background-position: -680px -460px; }
+.emoji-on { background-position: -680px -480px; }
+.emoji-oncoming_automobile { background-position: -680px -500px; }
+.emoji-oncoming_bus { background-position: -680px -520px; }
+.emoji-oncoming_police_car { background-position: -680px -540px; }
+.emoji-oncoming_taxi { background-position: -680px -560px; }
+.emoji-one { background-position: -680px -580px; }
+.emoji-open_file_folder { background-position: -680px -600px; }
+.emoji-open_hands { background-position: -680px -620px; }
+.emoji-open_hands_tone1 { background-position: -680px -640px; }
+.emoji-open_hands_tone2 { background-position: -680px -660px; }
+.emoji-open_hands_tone3 { background-position: 0 -680px; }
+.emoji-open_hands_tone4 { background-position: -20px -680px; }
+.emoji-open_hands_tone5 { background-position: -40px -680px; }
+.emoji-open_mouth { background-position: -60px -680px; }
+.emoji-ophiuchus { background-position: -80px -680px; }
+.emoji-orange_book { background-position: -100px -680px; }
+.emoji-orthodox_cross { background-position: -120px -680px; }
+.emoji-outbox_tray { background-position: -140px -680px; }
+.emoji-owl { background-position: -160px -680px; }
+.emoji-ox { background-position: -180px -680px; }
+.emoji-package { background-position: -200px -680px; }
+.emoji-page_facing_up { background-position: -220px -680px; }
+.emoji-page_with_curl { background-position: -240px -680px; }
+.emoji-pager { background-position: -260px -680px; }
+.emoji-paintbrush { background-position: -280px -680px; }
+.emoji-palm_tree { background-position: -300px -680px; }
+.emoji-pancakes { background-position: -320px -680px; }
+.emoji-panda_face { background-position: -340px -680px; }
+.emoji-paperclip { background-position: -360px -680px; }
+.emoji-paperclips { background-position: -380px -680px; }
+.emoji-park { background-position: -400px -680px; }
+.emoji-parking { background-position: -420px -680px; }
+.emoji-part_alternation_mark { background-position: -440px -680px; }
+.emoji-partly_sunny { background-position: -460px -680px; }
+.emoji-passport_control { background-position: -480px -680px; }
+.emoji-pause_button { background-position: -500px -680px; }
+.emoji-peace { background-position: -520px -680px; }
+.emoji-peach { background-position: -540px -680px; }
+.emoji-peanuts { background-position: -560px -680px; }
+.emoji-pear { background-position: -580px -680px; }
+.emoji-pen_ballpoint { background-position: -600px -680px; }
+.emoji-pen_fountain { background-position: -620px -680px; }
+.emoji-pencil { background-position: -640px -680px; }
+.emoji-pencil2 { background-position: -660px -680px; }
+.emoji-penguin { background-position: -680px -680px; }
+.emoji-pensive { background-position: -700px 0; }
+.emoji-performing_arts { background-position: -700px -20px; }
+.emoji-persevere { background-position: -700px -40px; }
+.emoji-person_frowning { background-position: -700px -60px; }
+.emoji-person_frowning_tone1 { background-position: -700px -80px; }
+.emoji-person_frowning_tone2 { background-position: -700px -100px; }
+.emoji-person_frowning_tone3 { background-position: -700px -120px; }
+.emoji-person_frowning_tone4 { background-position: -700px -140px; }
+.emoji-person_frowning_tone5 { background-position: -700px -160px; }
+.emoji-person_with_blond_hair { background-position: -700px -180px; }
+.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; }
+.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; }
+.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; }
+.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; }
+.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; }
+.emoji-person_with_pouting_face { background-position: -700px -300px; }
+.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; }
+.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; }
+.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; }
+.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; }
+.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; }
+.emoji-pick { background-position: -700px -420px; }
+.emoji-pig { background-position: -700px -440px; }
+.emoji-pig2 { background-position: -700px -460px; }
+.emoji-pig_nose { background-position: -700px -480px; }
+.emoji-pill { background-position: -700px -500px; }
+.emoji-pineapple { background-position: -700px -520px; }
+.emoji-ping_pong { background-position: -700px -540px; }
+.emoji-pisces { background-position: -700px -560px; }
+.emoji-pizza { background-position: -700px -580px; }
+.emoji-place_of_worship { background-position: -700px -600px; }
+.emoji-play_pause { background-position: -700px -620px; }
+.emoji-point_down { background-position: -700px -640px; }
+.emoji-point_down_tone1 { background-position: -700px -660px; }
+.emoji-point_down_tone2 { background-position: -700px -680px; }
+.emoji-point_down_tone3 { background-position: 0 -700px; }
+.emoji-point_down_tone4 { background-position: -20px -700px; }
+.emoji-point_down_tone5 { background-position: -40px -700px; }
+.emoji-point_left { background-position: -60px -700px; }
+.emoji-point_left_tone1 { background-position: -80px -700px; }
+.emoji-point_left_tone2 { background-position: -100px -700px; }
+.emoji-point_left_tone3 { background-position: -120px -700px; }
+.emoji-point_left_tone4 { background-position: -140px -700px; }
+.emoji-point_left_tone5 { background-position: -160px -700px; }
+.emoji-point_right { background-position: -180px -700px; }
+.emoji-point_right_tone1 { background-position: -200px -700px; }
+.emoji-point_right_tone2 { background-position: -220px -700px; }
+.emoji-point_right_tone3 { background-position: -240px -700px; }
+.emoji-point_right_tone4 { background-position: -260px -700px; }
+.emoji-point_right_tone5 { background-position: -280px -700px; }
+.emoji-point_up { background-position: -300px -700px; }
+.emoji-point_up_2 { background-position: -320px -700px; }
+.emoji-point_up_2_tone1 { background-position: -340px -700px; }
+.emoji-point_up_2_tone2 { background-position: -360px -700px; }
+.emoji-point_up_2_tone3 { background-position: -380px -700px; }
+.emoji-point_up_2_tone4 { background-position: -400px -700px; }
+.emoji-point_up_2_tone5 { background-position: -420px -700px; }
+.emoji-point_up_tone1 { background-position: -440px -700px; }
+.emoji-point_up_tone2 { background-position: -460px -700px; }
+.emoji-point_up_tone3 { background-position: -480px -700px; }
+.emoji-point_up_tone4 { background-position: -500px -700px; }
+.emoji-point_up_tone5 { background-position: -520px -700px; }
+.emoji-police_car { background-position: -540px -700px; }
+.emoji-poodle { background-position: -560px -700px; }
+.emoji-poop { background-position: -580px -700px; }
+.emoji-popcorn { background-position: -600px -700px; }
+.emoji-post_office { background-position: -620px -700px; }
+.emoji-postal_horn { background-position: -640px -700px; }
+.emoji-postbox { background-position: -660px -700px; }
+.emoji-potable_water { background-position: -680px -700px; }
+.emoji-potato { background-position: -700px -700px; }
+.emoji-pouch { background-position: -720px 0; }
+.emoji-poultry_leg { background-position: -720px -20px; }
+.emoji-pound { background-position: -720px -40px; }
+.emoji-pouting_cat { background-position: -720px -60px; }
+.emoji-pray { background-position: -720px -80px; }
+.emoji-pray_tone1 { background-position: -720px -100px; }
+.emoji-pray_tone2 { background-position: -720px -120px; }
+.emoji-pray_tone3 { background-position: -720px -140px; }
+.emoji-pray_tone4 { background-position: -720px -160px; }
+.emoji-pray_tone5 { background-position: -720px -180px; }
+.emoji-prayer_beads { background-position: -720px -200px; }
+.emoji-pregnant_woman { background-position: -720px -220px; }
+.emoji-pregnant_woman_tone1 { background-position: -720px -240px; }
+.emoji-pregnant_woman_tone2 { background-position: -720px -260px; }
+.emoji-pregnant_woman_tone3 { background-position: -720px -280px; }
+.emoji-pregnant_woman_tone4 { background-position: -720px -300px; }
+.emoji-pregnant_woman_tone5 { background-position: -720px -320px; }
+.emoji-prince { background-position: -720px -340px; }
+.emoji-prince_tone1 { background-position: -720px -360px; }
+.emoji-prince_tone2 { background-position: -720px -380px; }
+.emoji-prince_tone3 { background-position: -720px -400px; }
+.emoji-prince_tone4 { background-position: -720px -420px; }
+.emoji-prince_tone5 { background-position: -720px -440px; }
+.emoji-princess { background-position: -720px -460px; }
+.emoji-princess_tone1 { background-position: -720px -480px; }
+.emoji-princess_tone2 { background-position: -720px -500px; }
+.emoji-princess_tone3 { background-position: -720px -520px; }
+.emoji-princess_tone4 { background-position: -720px -540px; }
+.emoji-princess_tone5 { background-position: -720px -560px; }
+.emoji-printer { background-position: -720px -580px; }
+.emoji-projector { background-position: -720px -600px; }
+.emoji-punch { background-position: -720px -620px; }
+.emoji-punch_tone1 { background-position: -720px -640px; }
+.emoji-punch_tone2 { background-position: -720px -660px; }
+.emoji-punch_tone3 { background-position: -720px -680px; }
+.emoji-punch_tone4 { background-position: -720px -700px; }
+.emoji-punch_tone5 { background-position: 0 -720px; }
+.emoji-purple_heart { background-position: -20px -720px; }
+.emoji-purse { background-position: -40px -720px; }
+.emoji-pushpin { background-position: -60px -720px; }
+.emoji-put_litter_in_its_place { background-position: -80px -720px; }
+.emoji-question { background-position: -100px -720px; }
+.emoji-rabbit { background-position: -120px -720px; }
+.emoji-rabbit2 { background-position: -140px -720px; }
+.emoji-race_car { background-position: -160px -720px; }
+.emoji-racehorse { background-position: -180px -720px; }
+.emoji-radio { background-position: -200px -720px; }
+.emoji-radio_button { background-position: -220px -720px; }
+.emoji-radioactive { background-position: -240px -720px; }
+.emoji-rage { background-position: -260px -720px; }
+.emoji-railway_car { background-position: -280px -720px; }
+.emoji-railway_track { background-position: -300px -720px; }
+.emoji-rainbow { background-position: -320px -720px; }
+.emoji-raised_back_of_hand { background-position: -340px -720px; }
+.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; }
+.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; }
+.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; }
+.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; }
+.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; }
+.emoji-raised_hand { background-position: -460px -720px; }
+.emoji-raised_hand_tone1 { background-position: -480px -720px; }
+.emoji-raised_hand_tone2 { background-position: -500px -720px; }
+.emoji-raised_hand_tone3 { background-position: -520px -720px; }
+.emoji-raised_hand_tone4 { background-position: -540px -720px; }
+.emoji-raised_hand_tone5 { background-position: -560px -720px; }
+.emoji-raised_hands { background-position: -580px -720px; }
+.emoji-raised_hands_tone1 { background-position: -600px -720px; }
+.emoji-raised_hands_tone2 { background-position: -620px -720px; }
+.emoji-raised_hands_tone3 { background-position: -640px -720px; }
+.emoji-raised_hands_tone4 { background-position: -660px -720px; }
+.emoji-raised_hands_tone5 { background-position: -680px -720px; }
+.emoji-raising_hand { background-position: -700px -720px; }
+.emoji-raising_hand_tone1 { background-position: -720px -720px; }
+.emoji-raising_hand_tone2 { background-position: -740px 0; }
+.emoji-raising_hand_tone3 { background-position: -740px -20px; }
+.emoji-raising_hand_tone4 { background-position: -740px -40px; }
+.emoji-raising_hand_tone5 { background-position: -740px -60px; }
+.emoji-ram { background-position: -740px -80px; }
+.emoji-ramen { background-position: -740px -100px; }
+.emoji-rat { background-position: -740px -120px; }
+.emoji-record_button { background-position: -740px -140px; }
+.emoji-recycle { background-position: -740px -160px; }
+.emoji-red_car { background-position: -740px -180px; }
+.emoji-red_circle { background-position: -740px -200px; }
+.emoji-registered { background-position: -740px -220px; }
+.emoji-relaxed { background-position: -740px -240px; }
+.emoji-relieved { background-position: -740px -260px; }
+.emoji-reminder_ribbon { background-position: -740px -280px; }
+.emoji-repeat { background-position: -740px -300px; }
+.emoji-repeat_one { background-position: -740px -320px; }
+.emoji-restroom { background-position: -740px -340px; }
+.emoji-revolving_hearts { background-position: -740px -360px; }
+.emoji-rewind { background-position: -740px -380px; }
+.emoji-rhino { background-position: -740px -400px; }
+.emoji-ribbon { background-position: -740px -420px; }
+.emoji-rice { background-position: -740px -440px; }
+.emoji-rice_ball { background-position: -740px -460px; }
+.emoji-rice_cracker { background-position: -740px -480px; }
+.emoji-rice_scene { background-position: -740px -500px; }
+.emoji-right_facing_fist { background-position: -740px -520px; }
+.emoji-right_facing_fist_tone1 { background-position: -740px -540px; }
+.emoji-right_facing_fist_tone2 { background-position: -740px -560px; }
+.emoji-right_facing_fist_tone3 { background-position: -740px -580px; }
+.emoji-right_facing_fist_tone4 { background-position: -740px -600px; }
+.emoji-right_facing_fist_tone5 { background-position: -740px -620px; }
+.emoji-ring { background-position: -740px -640px; }
+.emoji-robot { background-position: -740px -660px; }
+.emoji-rocket { background-position: -740px -680px; }
+.emoji-rofl { background-position: -740px -700px; }
+.emoji-roller_coaster { background-position: -740px -720px; }
+.emoji-rolling_eyes { background-position: 0 -740px; }
+.emoji-rooster { background-position: -20px -740px; }
+.emoji-rose { background-position: -40px -740px; }
+.emoji-rosette { background-position: -60px -740px; }
+.emoji-rotating_light { background-position: -80px -740px; }
+.emoji-round_pushpin { background-position: -100px -740px; }
+.emoji-rowboat { background-position: -120px -740px; }
+.emoji-rowboat_tone1 { background-position: -140px -740px; }
+.emoji-rowboat_tone2 { background-position: -160px -740px; }
+.emoji-rowboat_tone3 { background-position: -180px -740px; }
+.emoji-rowboat_tone4 { background-position: -200px -740px; }
+.emoji-rowboat_tone5 { background-position: -220px -740px; }
+.emoji-rugby_football { background-position: -240px -740px; }
+.emoji-runner { background-position: -260px -740px; }
+.emoji-runner_tone1 { background-position: -280px -740px; }
+.emoji-runner_tone2 { background-position: -300px -740px; }
+.emoji-runner_tone3 { background-position: -320px -740px; }
+.emoji-runner_tone4 { background-position: -340px -740px; }
+.emoji-runner_tone5 { background-position: -360px -740px; }
+.emoji-running_shirt_with_sash { background-position: -380px -740px; }
+.emoji-sa { background-position: -400px -740px; }
+.emoji-sagittarius { background-position: -420px -740px; }
+.emoji-sailboat { background-position: -440px -740px; }
+.emoji-sake { background-position: -460px -740px; }
+.emoji-salad { background-position: -480px -740px; }
+.emoji-sandal { background-position: -500px -740px; }
+.emoji-santa { background-position: -520px -740px; }
+.emoji-santa_tone1 { background-position: -540px -740px; }
+.emoji-santa_tone2 { background-position: -560px -740px; }
+.emoji-santa_tone3 { background-position: -580px -740px; }
+.emoji-santa_tone4 { background-position: -600px -740px; }
+.emoji-santa_tone5 { background-position: -620px -740px; }
+.emoji-satellite { background-position: -640px -740px; }
+.emoji-satellite_orbital { background-position: -660px -740px; }
+.emoji-saxophone { background-position: -680px -740px; }
+.emoji-scales { background-position: -700px -740px; }
+.emoji-school { background-position: -720px -740px; }
+.emoji-school_satchel { background-position: -740px -740px; }
+.emoji-scissors { background-position: -760px 0; }
+.emoji-scooter { background-position: -760px -20px; }
+.emoji-scorpion { background-position: -760px -40px; }
+.emoji-scorpius { background-position: -760px -60px; }
+.emoji-scream { background-position: -760px -80px; }
+.emoji-scream_cat { background-position: -760px -100px; }
+.emoji-scroll { background-position: -760px -120px; }
+.emoji-seat { background-position: -760px -140px; }
+.emoji-second_place { background-position: -760px -160px; }
+.emoji-secret { background-position: -760px -180px; }
+.emoji-see_no_evil { background-position: -760px -200px; }
+.emoji-seedling { background-position: -760px -220px; }
+.emoji-selfie { background-position: -760px -240px; }
+.emoji-selfie_tone1 { background-position: -760px -260px; }
+.emoji-selfie_tone2 { background-position: -760px -280px; }
+.emoji-selfie_tone3 { background-position: -760px -300px; }
+.emoji-selfie_tone4 { background-position: -760px -320px; }
+.emoji-selfie_tone5 { background-position: -760px -340px; }
+.emoji-seven { background-position: -760px -360px; }
+.emoji-shallow_pan_of_food { background-position: -760px -380px; }
+.emoji-shamrock { background-position: -760px -400px; }
+.emoji-shark { background-position: -760px -420px; }
+.emoji-shaved_ice { background-position: -760px -440px; }
+.emoji-sheep { background-position: -760px -460px; }
+.emoji-shell { background-position: -760px -480px; }
+.emoji-shield { background-position: -760px -500px; }
+.emoji-shinto_shrine { background-position: -760px -520px; }
+.emoji-ship { background-position: -760px -540px; }
+.emoji-shirt { background-position: -760px -560px; }
+.emoji-shopping_bags { background-position: -760px -580px; }
+.emoji-shopping_cart { background-position: -760px -600px; }
+.emoji-shower { background-position: -760px -620px; }
+.emoji-shrimp { background-position: -760px -640px; }
+.emoji-shrug { background-position: -760px -660px; }
+.emoji-shrug_tone1 { background-position: -760px -680px; }
+.emoji-shrug_tone2 { background-position: -760px -700px; }
+.emoji-shrug_tone3 { background-position: -760px -720px; }
+.emoji-shrug_tone4 { background-position: -760px -740px; }
+.emoji-shrug_tone5 { background-position: 0 -760px; }
+.emoji-signal_strength { background-position: -20px -760px; }
+.emoji-six { background-position: -40px -760px; }
+.emoji-six_pointed_star { background-position: -60px -760px; }
+.emoji-ski { background-position: -80px -760px; }
+.emoji-skier { background-position: -100px -760px; }
+.emoji-skull { background-position: -120px -760px; }
+.emoji-skull_crossbones { background-position: -140px -760px; }
+.emoji-sleeping { background-position: -160px -760px; }
+.emoji-sleeping_accommodation { background-position: -180px -760px; }
+.emoji-sleepy { background-position: -200px -760px; }
+.emoji-slight_frown { background-position: -220px -760px; }
+.emoji-slight_smile { background-position: -240px -760px; }
+.emoji-slot_machine { background-position: -260px -760px; }
+.emoji-small_blue_diamond { background-position: -280px -760px; }
+.emoji-small_orange_diamond { background-position: -300px -760px; }
+.emoji-small_red_triangle { background-position: -320px -760px; }
+.emoji-small_red_triangle_down { background-position: -340px -760px; }
+.emoji-smile { background-position: -360px -760px; }
+.emoji-smile_cat { background-position: -380px -760px; }
+.emoji-smiley { background-position: -400px -760px; }
+.emoji-smiley_cat { background-position: -420px -760px; }
+.emoji-smiling_imp { background-position: -440px -760px; }
+.emoji-smirk { background-position: -460px -760px; }
+.emoji-smirk_cat { background-position: -480px -760px; }
+.emoji-smoking { background-position: -500px -760px; }
+.emoji-snail { background-position: -520px -760px; }
+.emoji-snake { background-position: -540px -760px; }
+.emoji-sneezing_face { background-position: -560px -760px; }
+.emoji-snowboarder { background-position: -580px -760px; }
+.emoji-snowflake { background-position: -600px -760px; }
+.emoji-snowman { background-position: -620px -760px; }
+.emoji-snowman2 { background-position: -640px -760px; }
+.emoji-sob { background-position: -660px -760px; }
+.emoji-soccer { background-position: -680px -760px; }
+.emoji-soon { background-position: -700px -760px; }
+.emoji-sos { background-position: -720px -760px; }
+.emoji-sound { background-position: -740px -760px; }
+.emoji-space_invader { background-position: -760px -760px; }
+.emoji-spades { background-position: -780px 0; }
+.emoji-spaghetti { background-position: -780px -20px; }
+.emoji-sparkle { background-position: -780px -40px; }
+.emoji-sparkler { background-position: -780px -60px; }
+.emoji-sparkles { background-position: -780px -80px; }
+.emoji-sparkling_heart { background-position: -780px -100px; }
+.emoji-speak_no_evil { background-position: -780px -120px; }
+.emoji-speaker { background-position: -780px -140px; }
+.emoji-speaking_head { background-position: -780px -160px; }
+.emoji-speech_balloon { background-position: -780px -180px; }
+.emoji-speedboat { background-position: -780px -200px; }
+.emoji-spider { background-position: -780px -220px; }
+.emoji-spider_web { background-position: -780px -240px; }
+.emoji-spoon { background-position: -780px -260px; }
+.emoji-spy { background-position: -780px -280px; }
+.emoji-spy_tone1 { background-position: -780px -300px; }
+.emoji-spy_tone2 { background-position: -780px -320px; }
+.emoji-spy_tone3 { background-position: -780px -340px; }
+.emoji-spy_tone4 { background-position: -780px -360px; }
+.emoji-spy_tone5 { background-position: -780px -380px; }
+.emoji-squid { background-position: -780px -400px; }
+.emoji-stadium { background-position: -780px -420px; }
+.emoji-star { background-position: -780px -440px; }
+.emoji-star2 { background-position: -780px -460px; }
+.emoji-star_and_crescent { background-position: -780px -480px; }
+.emoji-star_of_david { background-position: -780px -500px; }
+.emoji-stars { background-position: -780px -520px; }
+.emoji-station { background-position: -780px -540px; }
+.emoji-statue_of_liberty { background-position: -780px -560px; }
+.emoji-steam_locomotive { background-position: -780px -580px; }
+.emoji-stew { background-position: -780px -600px; }
+.emoji-stop_button { background-position: -780px -620px; }
+.emoji-stopwatch { background-position: -780px -640px; }
+.emoji-straight_ruler { background-position: -780px -660px; }
+.emoji-strawberry { background-position: -780px -680px; }
+.emoji-stuck_out_tongue { background-position: -780px -700px; }
+.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; }
+.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; }
+.emoji-stuffed_flatbread { background-position: -780px -760px; }
+.emoji-sun_with_face { background-position: 0 -780px; }
+.emoji-sunflower { background-position: -20px -780px; }
+.emoji-sunglasses { background-position: -40px -780px; }
+.emoji-sunny { background-position: -60px -780px; }
+.emoji-sunrise { background-position: -80px -780px; }
+.emoji-sunrise_over_mountains { background-position: -100px -780px; }
+.emoji-surfer { background-position: -120px -780px; }
+.emoji-surfer_tone1 { background-position: -140px -780px; }
+.emoji-surfer_tone2 { background-position: -160px -780px; }
+.emoji-surfer_tone3 { background-position: -180px -780px; }
+.emoji-surfer_tone4 { background-position: -200px -780px; }
+.emoji-surfer_tone5 { background-position: -220px -780px; }
+.emoji-sushi { background-position: -240px -780px; }
+.emoji-suspension_railway { background-position: -260px -780px; }
+.emoji-sweat { background-position: -280px -780px; }
+.emoji-sweat_drops { background-position: -300px -780px; }
+.emoji-sweat_smile { background-position: -320px -780px; }
+.emoji-sweet_potato { background-position: -340px -780px; }
+.emoji-swimmer { background-position: -360px -780px; }
+.emoji-swimmer_tone1 { background-position: -380px -780px; }
+.emoji-swimmer_tone2 { background-position: -400px -780px; }
+.emoji-swimmer_tone3 { background-position: -420px -780px; }
+.emoji-swimmer_tone4 { background-position: -440px -780px; }
+.emoji-swimmer_tone5 { background-position: -460px -780px; }
+.emoji-symbols { background-position: -480px -780px; }
+.emoji-synagogue { background-position: -500px -780px; }
+.emoji-syringe { background-position: -520px -780px; }
+.emoji-taco { background-position: -540px -780px; }
+.emoji-tada { background-position: -560px -780px; }
+.emoji-tanabata_tree { background-position: -580px -780px; }
+.emoji-tangerine { background-position: -600px -780px; }
+.emoji-taurus { background-position: -620px -780px; }
+.emoji-taxi { background-position: -640px -780px; }
+.emoji-tea { background-position: -660px -780px; }
+.emoji-telephone { background-position: -680px -780px; }
+.emoji-telephone_receiver { background-position: -700px -780px; }
+.emoji-telescope { background-position: -720px -780px; }
+.emoji-ten { background-position: -740px -780px; }
+.emoji-tennis { background-position: -760px -780px; }
+.emoji-tent { background-position: -780px -780px; }
+.emoji-thermometer { background-position: -800px 0; }
+.emoji-thermometer_face { background-position: -800px -20px; }
+.emoji-thinking { background-position: -800px -40px; }
+.emoji-third_place { background-position: -800px -60px; }
+.emoji-thought_balloon { background-position: -800px -80px; }
+.emoji-three { background-position: -800px -100px; }
+.emoji-thumbsdown { background-position: -800px -120px; }
+.emoji-thumbsdown_tone1 { background-position: -800px -140px; }
+.emoji-thumbsdown_tone2 { background-position: -800px -160px; }
+.emoji-thumbsdown_tone3 { background-position: -800px -180px; }
+.emoji-thumbsdown_tone4 { background-position: -800px -200px; }
+.emoji-thumbsdown_tone5 { background-position: -800px -220px; }
+.emoji-thumbsup { background-position: -800px -240px; }
+.emoji-thumbsup_tone1 { background-position: -800px -260px; }
+.emoji-thumbsup_tone2 { background-position: -800px -280px; }
+.emoji-thumbsup_tone3 { background-position: -800px -300px; }
+.emoji-thumbsup_tone4 { background-position: -800px -320px; }
+.emoji-thumbsup_tone5 { background-position: -800px -340px; }
+.emoji-thunder_cloud_rain { background-position: -800px -360px; }
+.emoji-ticket { background-position: -800px -380px; }
+.emoji-tickets { background-position: -800px -400px; }
+.emoji-tiger { background-position: -800px -420px; }
+.emoji-tiger2 { background-position: -800px -440px; }
+.emoji-timer { background-position: -800px -460px; }
+.emoji-tired_face { background-position: -800px -480px; }
+.emoji-tm { background-position: -800px -500px; }
+.emoji-toilet { background-position: -800px -520px; }
+.emoji-tokyo_tower { background-position: -800px -540px; }
+.emoji-tomato { background-position: -800px -560px; }
+.emoji-tone1 { background-position: -800px -580px; }
+.emoji-tone2 { background-position: -800px -600px; }
+.emoji-tone3 { background-position: -800px -620px; }
+.emoji-tone4 { background-position: -800px -640px; }
+.emoji-tone5 { background-position: -800px -660px; }
+.emoji-tongue { background-position: -800px -680px; }
+.emoji-tools { background-position: -800px -700px; }
+.emoji-top { background-position: -800px -720px; }
+.emoji-tophat { background-position: -800px -740px; }
+.emoji-track_next { background-position: -800px -760px; }
+.emoji-track_previous { background-position: -800px -780px; }
+.emoji-trackball { background-position: 0 -800px; }
+.emoji-tractor { background-position: -20px -800px; }
+.emoji-traffic_light { background-position: -40px -800px; }
+.emoji-train { background-position: -60px -800px; }
+.emoji-train2 { background-position: -80px -800px; }
+.emoji-tram { background-position: -100px -800px; }
+.emoji-triangular_flag_on_post { background-position: -120px -800px; }
+.emoji-triangular_ruler { background-position: -140px -800px; }
+.emoji-trident { background-position: -160px -800px; }
+.emoji-triumph { background-position: -180px -800px; }
+.emoji-trolleybus { background-position: -200px -800px; }
+.emoji-trophy { background-position: -220px -800px; }
+.emoji-tropical_drink { background-position: -240px -800px; }
+.emoji-tropical_fish { background-position: -260px -800px; }
+.emoji-truck { background-position: -280px -800px; }
+.emoji-trumpet { background-position: -300px -800px; }
+.emoji-tulip { background-position: -320px -800px; }
+.emoji-tumbler_glass { background-position: -340px -800px; }
+.emoji-turkey { background-position: -360px -800px; }
+.emoji-turtle { background-position: -380px -800px; }
+.emoji-tv { background-position: -400px -800px; }
+.emoji-twisted_rightwards_arrows { background-position: -420px -800px; }
+.emoji-two { background-position: -440px -800px; }
+.emoji-two_hearts { background-position: -460px -800px; }
+.emoji-two_men_holding_hands { background-position: -480px -800px; }
+.emoji-two_women_holding_hands { background-position: -500px -800px; }
+.emoji-u5272 { background-position: -520px -800px; }
+.emoji-u5408 { background-position: -540px -800px; }
+.emoji-u55b6 { background-position: -560px -800px; }
+.emoji-u6307 { background-position: -580px -800px; }
+.emoji-u6708 { background-position: -600px -800px; }
+.emoji-u6709 { background-position: -620px -800px; }
+.emoji-u6e80 { background-position: -640px -800px; }
+.emoji-u7121 { background-position: -660px -800px; }
+.emoji-u7533 { background-position: -680px -800px; }
+.emoji-u7981 { background-position: -700px -800px; }
+.emoji-u7a7a { background-position: -720px -800px; }
+.emoji-umbrella { background-position: -740px -800px; }
+.emoji-umbrella2 { background-position: -760px -800px; }
+.emoji-unamused { background-position: -780px -800px; }
+.emoji-underage { background-position: -800px -800px; }
+.emoji-unicorn { background-position: -820px 0; }
+.emoji-unlock { background-position: -820px -20px; }
+.emoji-up { background-position: -820px -40px; }
+.emoji-upside_down { background-position: -820px -60px; }
+.emoji-urn { background-position: -820px -80px; }
+.emoji-v { background-position: -820px -100px; }
+.emoji-v_tone1 { background-position: -820px -120px; }
+.emoji-v_tone2 { background-position: -820px -140px; }
+.emoji-v_tone3 { background-position: -820px -160px; }
+.emoji-v_tone4 { background-position: -820px -180px; }
+.emoji-v_tone5 { background-position: -820px -200px; }
+.emoji-vertical_traffic_light { background-position: -820px -220px; }
+.emoji-vhs { background-position: -820px -240px; }
+.emoji-vibration_mode { background-position: -820px -260px; }
+.emoji-video_camera { background-position: -820px -280px; }
+.emoji-video_game { background-position: -820px -300px; }
+.emoji-violin { background-position: -820px -320px; }
+.emoji-virgo { background-position: -820px -340px; }
+.emoji-volcano { background-position: -820px -360px; }
+.emoji-volleyball { background-position: -820px -380px; }
+.emoji-vs { background-position: -820px -400px; }
+.emoji-vulcan { background-position: -820px -420px; }
+.emoji-vulcan_tone1 { background-position: -820px -440px; }
+.emoji-vulcan_tone2 { background-position: -820px -460px; }
+.emoji-vulcan_tone3 { background-position: -820px -480px; }
+.emoji-vulcan_tone4 { background-position: -820px -500px; }
+.emoji-vulcan_tone5 { background-position: -820px -520px; }
+.emoji-walking { background-position: -820px -540px; }
+.emoji-walking_tone1 { background-position: -820px -560px; }
+.emoji-walking_tone2 { background-position: -820px -580px; }
+.emoji-walking_tone3 { background-position: -820px -600px; }
+.emoji-walking_tone4 { background-position: -820px -620px; }
+.emoji-walking_tone5 { background-position: -820px -640px; }
+.emoji-waning_crescent_moon { background-position: -820px -660px; }
+.emoji-waning_gibbous_moon { background-position: -820px -680px; }
+.emoji-warning { background-position: -820px -700px; }
+.emoji-wastebasket { background-position: -820px -720px; }
+.emoji-watch { background-position: -820px -740px; }
+.emoji-water_buffalo { background-position: -820px -760px; }
+.emoji-water_polo { background-position: -820px -780px; }
+.emoji-water_polo_tone1 { background-position: -820px -800px; }
+.emoji-water_polo_tone2 { background-position: 0 -820px; }
+.emoji-water_polo_tone3 { background-position: -20px -820px; }
+.emoji-water_polo_tone4 { background-position: -40px -820px; }
+.emoji-water_polo_tone5 { background-position: -60px -820px; }
+.emoji-watermelon { background-position: -80px -820px; }
+.emoji-wave { background-position: -100px -820px; }
+.emoji-wave_tone1 { background-position: -120px -820px; }
+.emoji-wave_tone2 { background-position: -140px -820px; }
+.emoji-wave_tone3 { background-position: -160px -820px; }
+.emoji-wave_tone4 { background-position: -180px -820px; }
+.emoji-wave_tone5 { background-position: -200px -820px; }
+.emoji-wavy_dash { background-position: -220px -820px; }
+.emoji-waxing_crescent_moon { background-position: -240px -820px; }
+.emoji-waxing_gibbous_moon { background-position: -260px -820px; }
+.emoji-wc { background-position: -280px -820px; }
+.emoji-weary { background-position: -300px -820px; }
+.emoji-wedding { background-position: -320px -820px; }
+.emoji-whale { background-position: -340px -820px; }
+.emoji-whale2 { background-position: -360px -820px; }
+.emoji-wheel_of_dharma { background-position: -380px -820px; }
+.emoji-wheelchair { background-position: -400px -820px; }
+.emoji-white_check_mark { background-position: -420px -820px; }
+.emoji-white_circle { background-position: -440px -820px; }
+.emoji-white_flower { background-position: -460px -820px; }
+.emoji-white_large_square { background-position: -480px -820px; }
+.emoji-white_medium_small_square { background-position: -500px -820px; }
+.emoji-white_medium_square { background-position: -520px -820px; }
+.emoji-white_small_square { background-position: -540px -820px; }
+.emoji-white_square_button { background-position: -560px -820px; }
+.emoji-white_sun_cloud { background-position: -580px -820px; }
+.emoji-white_sun_rain_cloud { background-position: -600px -820px; }
+.emoji-white_sun_small_cloud { background-position: -620px -820px; }
+.emoji-wilted_rose { background-position: -640px -820px; }
+.emoji-wind_blowing_face { background-position: -660px -820px; }
+.emoji-wind_chime { background-position: -680px -820px; }
+.emoji-wine_glass { background-position: -700px -820px; }
+.emoji-wink { background-position: -720px -820px; }
+.emoji-wolf { background-position: -740px -820px; }
+.emoji-woman { background-position: -760px -820px; }
+.emoji-woman_tone1 { background-position: -780px -820px; }
+.emoji-woman_tone2 { background-position: -800px -820px; }
+.emoji-woman_tone3 { background-position: -820px -820px; }
+.emoji-woman_tone4 { background-position: -840px 0; }
+.emoji-woman_tone5 { background-position: -840px -20px; }
+.emoji-womans_clothes { background-position: -840px -40px; }
+.emoji-womans_hat { background-position: -840px -60px; }
+.emoji-womens { background-position: -840px -80px; }
+.emoji-worried { background-position: -840px -100px; }
+.emoji-wrench { background-position: -840px -120px; }
+.emoji-wrestlers { background-position: -840px -140px; }
+.emoji-wrestlers_tone1 { background-position: -840px -160px; }
+.emoji-wrestlers_tone2 { background-position: -840px -180px; }
+.emoji-wrestlers_tone3 { background-position: -840px -200px; }
+.emoji-wrestlers_tone4 { background-position: -840px -220px; }
+.emoji-wrestlers_tone5 { background-position: -840px -240px; }
+.emoji-writing_hand { background-position: -840px -260px; }
+.emoji-writing_hand_tone1 { background-position: -840px -280px; }
+.emoji-writing_hand_tone2 { background-position: -840px -300px; }
+.emoji-writing_hand_tone3 { background-position: -840px -320px; }
+.emoji-writing_hand_tone4 { background-position: -840px -340px; }
+.emoji-writing_hand_tone5 { background-position: -840px -360px; }
+.emoji-x { background-position: -840px -380px; }
+.emoji-yellow_heart { background-position: -840px -400px; }
+.emoji-yen { background-position: -840px -420px; }
+.emoji-yin_yang { background-position: -840px -440px; }
+.emoji-yum { background-position: -840px -460px; }
+.emoji-zap { background-position: -840px -480px; }
+.emoji-zero { background-position: -840px -500px; }
+.emoji-zipper_mouth { background-position: -840px -520px; }
+.emoji-100 { background-position: -840px -540px; }
+
+.emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
+ height: 20px;
+ width: 20px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 860px 840px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 7158de65143..d86ae57cd9a 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,1809 +1,7 @@
-.emoji-0023-20E3 { background-position: 0 0; }
-.emoji-002A-20E3 { background-position: -20px 0; }
-.emoji-0030-20E3 { background-position: 0 -20px; }
-.emoji-0031-20E3 { background-position: -20px -20px; }
-.emoji-0032-20E3 { background-position: -40px 0; }
-.emoji-0033-20E3 { background-position: -40px -20px; }
-.emoji-0034-20E3 { background-position: 0 -40px; }
-.emoji-0035-20E3 { background-position: -20px -40px; }
-.emoji-0036-20E3 { background-position: -40px -40px; }
-.emoji-0037-20E3 { background-position: -60px 0; }
-.emoji-0038-20E3 { background-position: -60px -20px; }
-.emoji-0039-20E3 { background-position: -60px -40px; }
-.emoji-00A9 { background-position: 0 -60px; }
-.emoji-00AE { background-position: -20px -60px; }
-.emoji-1F004 { background-position: -40px -60px; }
-.emoji-1F0CF { background-position: -60px -60px; }
-.emoji-1F170 { background-position: -80px 0; }
-.emoji-1F171 { background-position: -80px -20px; }
-.emoji-1F17E { background-position: -80px -40px; }
-.emoji-1F17F { background-position: -80px -60px; }
-.emoji-1F18E { background-position: 0 -80px; }
-.emoji-1F191 { background-position: -20px -80px; }
-.emoji-1F192 { background-position: -40px -80px; }
-.emoji-1F193 { background-position: -60px -80px; }
-.emoji-1F194 { background-position: -80px -80px; }
-.emoji-1F195 { background-position: -100px 0; }
-.emoji-1F196 { background-position: -100px -20px; }
-.emoji-1F197 { background-position: -100px -40px; }
-.emoji-1F198 { background-position: -100px -60px; }
-.emoji-1F199 { background-position: -100px -80px; }
-.emoji-1F19A { background-position: 0 -100px; }
-.emoji-1F1E6-1F1E8 { background-position: -20px -100px; }
-.emoji-1F1E6-1F1E9 { background-position: -40px -100px; }
-.emoji-1F1E6-1F1EA { background-position: -60px -100px; }
-.emoji-1F1E6-1F1EB { background-position: -80px -100px; }
-.emoji-1F1E6-1F1EC { background-position: -100px -100px; }
-.emoji-1F1E6-1F1EE { background-position: -120px 0; }
-.emoji-1F1E6-1F1F1 { background-position: -120px -20px; }
-.emoji-1F1E6-1F1F2 { background-position: -120px -40px; }
-.emoji-1F1E6-1F1F4 { background-position: -120px -60px; }
-.emoji-1F1E6-1F1F6 { background-position: -120px -80px; }
-.emoji-1F1E6-1F1F7 { background-position: -120px -100px; }
-.emoji-1F1E6-1F1F8 { background-position: 0 -120px; }
-.emoji-1F1E6-1F1F9 { background-position: -20px -120px; }
-.emoji-1F1E6-1F1FA { background-position: -40px -120px; }
-.emoji-1F1E6-1F1FC { background-position: -60px -120px; }
-.emoji-1F1E6-1F1FD { background-position: -80px -120px; }
-.emoji-1F1E6-1F1FF { background-position: -100px -120px; }
-.emoji-1F1E7-1F1E6 { background-position: -120px -120px; }
-.emoji-1F1E7-1F1E7 { background-position: -140px 0; }
-.emoji-1F1E7-1F1E9 { background-position: -140px -20px; }
-.emoji-1F1E7-1F1EA { background-position: -140px -40px; }
-.emoji-1F1E7-1F1EB { background-position: -140px -60px; }
-.emoji-1F1E7-1F1EC { background-position: -140px -80px; }
-.emoji-1F1E7-1F1ED { background-position: -140px -100px; }
-.emoji-1F1E7-1F1EE { background-position: -140px -120px; }
-.emoji-1F1E7-1F1EF { background-position: 0 -140px; }
-.emoji-1F1E7-1F1F1 { background-position: -20px -140px; }
-.emoji-1F1E7-1F1F2 { background-position: -40px -140px; }
-.emoji-1F1E7-1F1F3 { background-position: -60px -140px; }
-.emoji-1F1E7-1F1F4 { background-position: -80px -140px; }
-.emoji-1F1E7-1F1F6 { background-position: -100px -140px; }
-.emoji-1F1E7-1F1F7 { background-position: -120px -140px; }
-.emoji-1F1E7-1F1F8 { background-position: -140px -140px; }
-.emoji-1F1E7-1F1F9 { background-position: -160px 0; }
-.emoji-1F1E7-1F1FB { background-position: -160px -20px; }
-.emoji-1F1E7-1F1FC { background-position: -160px -40px; }
-.emoji-1F1E7-1F1FE { background-position: -160px -60px; }
-.emoji-1F1E7-1F1FF { background-position: -160px -80px; }
-.emoji-1F1E8-1F1E6 { background-position: -160px -100px; }
-.emoji-1F1E8-1F1E8 { background-position: -160px -120px; }
-.emoji-1F1E8-1F1E9 { background-position: -160px -140px; }
-.emoji-1F1E8-1F1EB { background-position: 0 -160px; }
-.emoji-1F1E8-1F1EC { background-position: -20px -160px; }
-.emoji-1F1E8-1F1ED { background-position: -40px -160px; }
-.emoji-1F1E8-1F1EE { background-position: -60px -160px; }
-.emoji-1F1E8-1F1F0 { background-position: -80px -160px; }
-.emoji-1F1E8-1F1F1 { background-position: -100px -160px; }
-.emoji-1F1E8-1F1F2 { background-position: -120px -160px; }
-.emoji-1F1E8-1F1F3 { background-position: -140px -160px; }
-.emoji-1F1E8-1F1F4 { background-position: -160px -160px; }
-.emoji-1F1E8-1F1F5 { background-position: -180px 0; }
-.emoji-1F1E8-1F1F7 { background-position: -180px -20px; }
-.emoji-1F1E8-1F1FA { background-position: -180px -40px; }
-.emoji-1F1E8-1F1FB { background-position: -180px -60px; }
-.emoji-1F1E8-1F1FC { background-position: -180px -80px; }
-.emoji-1F1E8-1F1FD { background-position: -180px -100px; }
-.emoji-1F1E8-1F1FE { background-position: -180px -120px; }
-.emoji-1F1E8-1F1FF { background-position: -180px -140px; }
-.emoji-1F1E9-1F1EA { background-position: -180px -160px; }
-.emoji-1F1E9-1F1EC { background-position: 0 -180px; }
-.emoji-1F1E9-1F1EF { background-position: -20px -180px; }
-.emoji-1F1E9-1F1F0 { background-position: -40px -180px; }
-.emoji-1F1E9-1F1F2 { background-position: -60px -180px; }
-.emoji-1F1E9-1F1F4 { background-position: -80px -180px; }
-.emoji-1F1E9-1F1FF { background-position: -100px -180px; }
-.emoji-1F1EA-1F1E6 { background-position: -120px -180px; }
-.emoji-1F1EA-1F1E8 { background-position: -140px -180px; }
-.emoji-1F1EA-1F1EA { background-position: -160px -180px; }
-.emoji-1F1EA-1F1EC { background-position: -180px -180px; }
-.emoji-1F1EA-1F1ED { background-position: -200px 0; }
-.emoji-1F1EA-1F1F7 { background-position: -200px -20px; }
-.emoji-1F1EA-1F1F8 { background-position: -200px -40px; }
-.emoji-1F1EA-1F1F9 { background-position: -200px -60px; }
-.emoji-1F1EA-1F1FA { background-position: -200px -80px; }
-.emoji-1F1EB-1F1EE { background-position: -200px -100px; }
-.emoji-1F1EB-1F1EF { background-position: -200px -120px; }
-.emoji-1F1EB-1F1F0 { background-position: -200px -140px; }
-.emoji-1F1EB-1F1F2 { background-position: -200px -160px; }
-.emoji-1F1EB-1F1F4 { background-position: -200px -180px; }
-.emoji-1F1EB-1F1F7 { background-position: 0 -200px; }
-.emoji-1F1EC-1F1E6 { background-position: -20px -200px; }
-.emoji-1F1EC-1F1E7 { background-position: -40px -200px; }
-.emoji-1F1EC-1F1E9 { background-position: -60px -200px; }
-.emoji-1F1EC-1F1EA { background-position: -80px -200px; }
-.emoji-1F1EC-1F1EB { background-position: -100px -200px; }
-.emoji-1F1EC-1F1EC { background-position: -120px -200px; }
-.emoji-1F1EC-1F1ED { background-position: -140px -200px; }
-.emoji-1F1EC-1F1EE { background-position: -160px -200px; }
-.emoji-1F1EC-1F1F1 { background-position: -180px -200px; }
-.emoji-1F1EC-1F1F2 { background-position: -200px -200px; }
-.emoji-1F1EC-1F1F3 { background-position: -220px 0; }
-.emoji-1F1EC-1F1F5 { background-position: -220px -20px; }
-.emoji-1F1EC-1F1F6 { background-position: -220px -40px; }
-.emoji-1F1EC-1F1F7 { background-position: -220px -60px; }
-.emoji-1F1EC-1F1F8 { background-position: -220px -80px; }
-.emoji-1F1EC-1F1F9 { background-position: -220px -100px; }
-.emoji-1F1EC-1F1FA { background-position: -220px -120px; }
-.emoji-1F1EC-1F1FC { background-position: -220px -140px; }
-.emoji-1F1EC-1F1FE { background-position: -220px -160px; }
-.emoji-1F1ED-1F1F0 { background-position: -220px -180px; }
-.emoji-1F1ED-1F1F2 { background-position: -220px -200px; }
-.emoji-1F1ED-1F1F3 { background-position: 0 -220px; }
-.emoji-1F1ED-1F1F7 { background-position: -20px -220px; }
-.emoji-1F1ED-1F1F9 { background-position: -40px -220px; }
-.emoji-1F1ED-1F1FA { background-position: -60px -220px; }
-.emoji-1F1EE-1F1E8 { background-position: -80px -220px; }
-.emoji-1F1EE-1F1E9 { background-position: -100px -220px; }
-.emoji-1F1EE-1F1EA { background-position: -120px -220px; }
-.emoji-1F1EE-1F1F1 { background-position: -140px -220px; }
-.emoji-1F1EE-1F1F2 { background-position: -160px -220px; }
-.emoji-1F1EE-1F1F3 { background-position: -180px -220px; }
-.emoji-1F1EE-1F1F4 { background-position: -200px -220px; }
-.emoji-1F1EE-1F1F6 { background-position: -220px -220px; }
-.emoji-1F1EE-1F1F7 { background-position: -240px 0; }
-.emoji-1F1EE-1F1F8 { background-position: -240px -20px; }
-.emoji-1F1EE-1F1F9 { background-position: -240px -40px; }
-.emoji-1F1EF-1F1EA { background-position: -240px -60px; }
-.emoji-1F1EF-1F1F2 { background-position: -240px -80px; }
-.emoji-1F1EF-1F1F4 { background-position: -240px -100px; }
-.emoji-1F1EF-1F1F5 { background-position: -240px -120px; }
-.emoji-1F1F0-1F1EA { background-position: -240px -140px; }
-.emoji-1F1F0-1F1EC { background-position: -240px -160px; }
-.emoji-1F1F0-1F1ED { background-position: -240px -180px; }
-.emoji-1F1F0-1F1EE { background-position: -240px -200px; }
-.emoji-1F1F0-1F1F2 { background-position: -240px -220px; }
-.emoji-1F1F0-1F1F3 { background-position: 0 -240px; }
-.emoji-1F1F0-1F1F5 { background-position: -20px -240px; }
-.emoji-1F1F0-1F1F7 { background-position: -40px -240px; }
-.emoji-1F1F0-1F1FC { background-position: -60px -240px; }
-.emoji-1F1F0-1F1FE { background-position: -80px -240px; }
-.emoji-1F1F0-1F1FF { background-position: -100px -240px; }
-.emoji-1F1F1-1F1E6 { background-position: -120px -240px; }
-.emoji-1F1F1-1F1E7 { background-position: -140px -240px; }
-.emoji-1F1F1-1F1E8 { background-position: -160px -240px; }
-.emoji-1F1F1-1F1EE { background-position: -180px -240px; }
-.emoji-1F1F1-1F1F0 { background-position: -200px -240px; }
-.emoji-1F1F1-1F1F7 { background-position: -220px -240px; }
-.emoji-1F1F1-1F1F8 { background-position: -240px -240px; }
-.emoji-1F1F1-1F1F9 { background-position: -260px 0; }
-.emoji-1F1F1-1F1FA { background-position: -260px -20px; }
-.emoji-1F1F1-1F1FB { background-position: -260px -40px; }
-.emoji-1F1F1-1F1FE { background-position: -260px -60px; }
-.emoji-1F1F2-1F1E6 { background-position: -260px -80px; }
-.emoji-1F1F2-1F1E8 { background-position: -260px -100px; }
-.emoji-1F1F2-1F1E9 { background-position: -260px -120px; }
-.emoji-1F1F2-1F1EA { background-position: -260px -140px; }
-.emoji-1F1F2-1F1EB { background-position: -260px -160px; }
-.emoji-1F1F2-1F1EC { background-position: -260px -180px; }
-.emoji-1F1F2-1F1ED { background-position: -260px -200px; }
-.emoji-1F1F2-1F1F0 { background-position: -260px -220px; }
-.emoji-1F1F2-1F1F1 { background-position: -260px -240px; }
-.emoji-1F1F2-1F1F2 { background-position: 0 -260px; }
-.emoji-1F1F2-1F1F3 { background-position: -20px -260px; }
-.emoji-1F1F2-1F1F4 { background-position: -40px -260px; }
-.emoji-1F1F2-1F1F5 { background-position: -60px -260px; }
-.emoji-1F1F2-1F1F6 { background-position: -80px -260px; }
-.emoji-1F1F2-1F1F7 { background-position: -100px -260px; }
-.emoji-1F1F2-1F1F8 { background-position: -120px -260px; }
-.emoji-1F1F2-1F1F9 { background-position: -140px -260px; }
-.emoji-1F1F2-1F1FA { background-position: -160px -260px; }
-.emoji-1F1F2-1F1FB { background-position: -180px -260px; }
-.emoji-1F1F2-1F1FC { background-position: -200px -260px; }
-.emoji-1F1F2-1F1FD { background-position: -220px -260px; }
-.emoji-1F1F2-1F1FE { background-position: -240px -260px; }
-.emoji-1F1F2-1F1FF { background-position: -260px -260px; }
-.emoji-1F1F3-1F1E6 { background-position: -280px 0; }
-.emoji-1F1F3-1F1E8 { background-position: -280px -20px; }
-.emoji-1F1F3-1F1EA { background-position: -280px -40px; }
-.emoji-1F1F3-1F1EB { background-position: -280px -60px; }
-.emoji-1F1F3-1F1EC { background-position: -280px -80px; }
-.emoji-1F1F3-1F1EE { background-position: -280px -100px; }
-.emoji-1F1F3-1F1F1 { background-position: -280px -120px; }
-.emoji-1F1F3-1F1F4 { background-position: -280px -140px; }
-.emoji-1F1F3-1F1F5 { background-position: -280px -160px; }
-.emoji-1F1F3-1F1F7 { background-position: -280px -180px; }
-.emoji-1F1F3-1F1FA { background-position: -280px -200px; }
-.emoji-1F1F3-1F1FF { background-position: -280px -220px; }
-.emoji-1F1F4-1F1F2 { background-position: -280px -240px; }
-.emoji-1F1F5-1F1E6 { background-position: -280px -260px; }
-.emoji-1F1F5-1F1EA { background-position: 0 -280px; }
-.emoji-1F1F5-1F1EB { background-position: -20px -280px; }
-.emoji-1F1F5-1F1EC { background-position: -40px -280px; }
-.emoji-1F1F5-1F1ED { background-position: -60px -280px; }
-.emoji-1F1F5-1F1F0 { background-position: -80px -280px; }
-.emoji-1F1F5-1F1F1 { background-position: -100px -280px; }
-.emoji-1F1F5-1F1F2 { background-position: -120px -280px; }
-.emoji-1F1F5-1F1F3 { background-position: -140px -280px; }
-.emoji-1F1F5-1F1F7 { background-position: -160px -280px; }
-.emoji-1F1F5-1F1F8 { background-position: -180px -280px; }
-.emoji-1F1F5-1F1F9 { background-position: -200px -280px; }
-.emoji-1F1F5-1F1FC { background-position: -220px -280px; }
-.emoji-1F1F5-1F1FE { background-position: -240px -280px; }
-.emoji-1F1F6-1F1E6 { background-position: -260px -280px; }
-.emoji-1F1F7-1F1EA { background-position: -280px -280px; }
-.emoji-1F1F7-1F1F4 { background-position: -300px 0; }
-.emoji-1F1F7-1F1F8 { background-position: -300px -20px; }
-.emoji-1F1F7-1F1FA { background-position: -300px -40px; }
-.emoji-1F1F7-1F1FC { background-position: -300px -60px; }
-.emoji-1F1F8-1F1E6 { background-position: -300px -80px; }
-.emoji-1F1F8-1F1E7 { background-position: -300px -100px; }
-.emoji-1F1F8-1F1E8 { background-position: -300px -120px; }
-.emoji-1F1F8-1F1E9 { background-position: -300px -140px; }
-.emoji-1F1F8-1F1EA { background-position: -300px -160px; }
-.emoji-1F1F8-1F1EC { background-position: -300px -180px; }
-.emoji-1F1F8-1F1ED { background-position: -300px -200px; }
-.emoji-1F1F8-1F1EE { background-position: -300px -220px; }
-.emoji-1F1F8-1F1EF { background-position: -300px -240px; }
-.emoji-1F1F8-1F1F0 { background-position: -300px -260px; }
-.emoji-1F1F8-1F1F1 { background-position: -300px -280px; }
-.emoji-1F1F8-1F1F2 { background-position: 0 -300px; }
-.emoji-1F1F8-1F1F3 { background-position: -20px -300px; }
-.emoji-1F1F8-1F1F4 { background-position: -40px -300px; }
-.emoji-1F1F8-1F1F7 { background-position: -60px -300px; }
-.emoji-1F1F8-1F1F8 { background-position: -80px -300px; }
-.emoji-1F1F8-1F1F9 { background-position: -100px -300px; }
-.emoji-1F1F8-1F1FB { background-position: -120px -300px; }
-.emoji-1F1F8-1F1FD { background-position: -140px -300px; }
-.emoji-1F1F8-1F1FE { background-position: -160px -300px; }
-.emoji-1F1F8-1F1FF { background-position: -180px -300px; }
-.emoji-1F1F9-1F1E6 { background-position: -200px -300px; }
-.emoji-1F1F9-1F1E8 { background-position: -220px -300px; }
-.emoji-1F1F9-1F1E9 { background-position: -240px -300px; }
-.emoji-1F1F9-1F1EB { background-position: -260px -300px; }
-.emoji-1F1F9-1F1EC { background-position: -280px -300px; }
-.emoji-1F1F9-1F1ED { background-position: -300px -300px; }
-.emoji-1F1F9-1F1EF { background-position: -320px 0; }
-.emoji-1F1F9-1F1F0 { background-position: -320px -20px; }
-.emoji-1F1F9-1F1F1 { background-position: -320px -40px; }
-.emoji-1F1F9-1F1F2 { background-position: -320px -60px; }
-.emoji-1F1F9-1F1F3 { background-position: -320px -80px; }
-.emoji-1F1F9-1F1F4 { background-position: -320px -100px; }
-.emoji-1F1F9-1F1F7 { background-position: -320px -120px; }
-.emoji-1F1F9-1F1F9 { background-position: -320px -140px; }
-.emoji-1F1F9-1F1FB { background-position: -320px -160px; }
-.emoji-1F1F9-1F1FC { background-position: -320px -180px; }
-.emoji-1F1F9-1F1FF { background-position: -320px -200px; }
-.emoji-1F1FA-1F1E6 { background-position: -320px -220px; }
-.emoji-1F1FA-1F1EC { background-position: -320px -240px; }
-.emoji-1F1FA-1F1F2 { background-position: -320px -260px; }
-.emoji-1F1FA-1F1F8 { background-position: -320px -280px; }
-.emoji-1F1FA-1F1FE { background-position: -320px -300px; }
-.emoji-1F1FA-1F1FF { background-position: 0 -320px; }
-.emoji-1F1FB-1F1E6 { background-position: -20px -320px; }
-.emoji-1F1FB-1F1E8 { background-position: -40px -320px; }
-.emoji-1F1FB-1F1EA { background-position: -60px -320px; }
-.emoji-1F1FB-1F1EC { background-position: -80px -320px; }
-.emoji-1F1FB-1F1EE { background-position: -100px -320px; }
-.emoji-1F1FB-1F1F3 { background-position: -120px -320px; }
-.emoji-1F1FB-1F1FA { background-position: -140px -320px; }
-.emoji-1F1FC-1F1EB { background-position: -160px -320px; }
-.emoji-1F1FC-1F1F8 { background-position: -180px -320px; }
-.emoji-1F1FD-1F1F0 { background-position: -200px -320px; }
-.emoji-1F1FE-1F1EA { background-position: -220px -320px; }
-.emoji-1F1FE-1F1F9 { background-position: -240px -320px; }
-.emoji-1F1FF-1F1E6 { background-position: -260px -320px; }
-.emoji-1F1FF-1F1F2 { background-position: -280px -320px; }
-.emoji-1F1FF-1F1FC { background-position: -300px -320px; }
-.emoji-1F201 { background-position: -320px -320px; }
-.emoji-1F202 { background-position: -340px 0; }
-.emoji-1F21A { background-position: -340px -20px; }
-.emoji-1F22F { background-position: -340px -40px; }
-.emoji-1F232 { background-position: -340px -60px; }
-.emoji-1F233 { background-position: -340px -80px; }
-.emoji-1F234 { background-position: -340px -100px; }
-.emoji-1F235 { background-position: -340px -120px; }
-.emoji-1F236 { background-position: -340px -140px; }
-.emoji-1F237 { background-position: -340px -160px; }
-.emoji-1F238 { background-position: -340px -180px; }
-.emoji-1F239 { background-position: -340px -200px; }
-.emoji-1F23A { background-position: -340px -220px; }
-.emoji-1F250 { background-position: -340px -240px; }
-.emoji-1F251 { background-position: -340px -260px; }
-.emoji-1F300 { background-position: -340px -280px; }
-.emoji-1F301 { background-position: -340px -300px; }
-.emoji-1F302 { background-position: -340px -320px; }
-.emoji-1F303 { background-position: 0 -340px; }
-.emoji-1F304 { background-position: -20px -340px; }
-.emoji-1F305 { background-position: -40px -340px; }
-.emoji-1F306 { background-position: -60px -340px; }
-.emoji-1F307 { background-position: -80px -340px; }
-.emoji-1F308 { background-position: -100px -340px; }
-.emoji-1F309 { background-position: -120px -340px; }
-.emoji-1F30A { background-position: -140px -340px; }
-.emoji-1F30B { background-position: -160px -340px; }
-.emoji-1F30C { background-position: -180px -340px; }
-.emoji-1F30D { background-position: -200px -340px; }
-.emoji-1F30E { background-position: -220px -340px; }
-.emoji-1F30F { background-position: -240px -340px; }
-.emoji-1F310 { background-position: -260px -340px; }
-.emoji-1F311 { background-position: -280px -340px; }
-.emoji-1F312 { background-position: -300px -340px; }
-.emoji-1F313 { background-position: -320px -340px; }
-.emoji-1F314 { background-position: -340px -340px; }
-.emoji-1F315 { background-position: -360px 0; }
-.emoji-1F316 { background-position: -360px -20px; }
-.emoji-1F317 { background-position: -360px -40px; }
-.emoji-1F318 { background-position: -360px -60px; }
-.emoji-1F319 { background-position: -360px -80px; }
-.emoji-1F31A { background-position: -360px -100px; }
-.emoji-1F31B { background-position: -360px -120px; }
-.emoji-1F31C { background-position: -360px -140px; }
-.emoji-1F31D { background-position: -360px -160px; }
-.emoji-1F31E { background-position: -360px -180px; }
-.emoji-1F31F { background-position: -360px -200px; }
-.emoji-1F320 { background-position: -360px -220px; }
-.emoji-1F321 { background-position: -360px -240px; }
-.emoji-1F324 { background-position: -360px -260px; }
-.emoji-1F325 { background-position: -360px -280px; }
-.emoji-1F326 { background-position: -360px -300px; }
-.emoji-1F327 { background-position: -360px -320px; }
-.emoji-1F328 { background-position: -360px -340px; }
-.emoji-1F329 { background-position: 0 -360px; }
-.emoji-1F32A { background-position: -20px -360px; }
-.emoji-1F32B { background-position: -40px -360px; }
-.emoji-1F32C { background-position: -60px -360px; }
-.emoji-1F32D { background-position: -80px -360px; }
-.emoji-1F32E { background-position: -100px -360px; }
-.emoji-1F32F { background-position: -120px -360px; }
-.emoji-1F330 { background-position: -140px -360px; }
-.emoji-1F331 { background-position: -160px -360px; }
-.emoji-1F332 { background-position: -180px -360px; }
-.emoji-1F333 { background-position: -200px -360px; }
-.emoji-1F334 { background-position: -220px -360px; }
-.emoji-1F335 { background-position: -240px -360px; }
-.emoji-1F336 { background-position: -260px -360px; }
-.emoji-1F337 { background-position: -280px -360px; }
-.emoji-1F338 { background-position: -300px -360px; }
-.emoji-1F339 { background-position: -320px -360px; }
-.emoji-1F33A { background-position: -340px -360px; }
-.emoji-1F33B { background-position: -360px -360px; }
-.emoji-1F33C { background-position: -380px 0; }
-.emoji-1F33D { background-position: -380px -20px; }
-.emoji-1F33E { background-position: -380px -40px; }
-.emoji-1F33F { background-position: -380px -60px; }
-.emoji-1F340 { background-position: -380px -80px; }
-.emoji-1F341 { background-position: -380px -100px; }
-.emoji-1F342 { background-position: -380px -120px; }
-.emoji-1F343 { background-position: -380px -140px; }
-.emoji-1F344 { background-position: -380px -160px; }
-.emoji-1F345 { background-position: -380px -180px; }
-.emoji-1F346 { background-position: -380px -200px; }
-.emoji-1F347 { background-position: -380px -220px; }
-.emoji-1F348 { background-position: -380px -240px; }
-.emoji-1F349 { background-position: -380px -260px; }
-.emoji-1F34A { background-position: -380px -280px; }
-.emoji-1F34B { background-position: -380px -300px; }
-.emoji-1F34C { background-position: -380px -320px; }
-.emoji-1F34D { background-position: -380px -340px; }
-.emoji-1F34E { background-position: -380px -360px; }
-.emoji-1F34F { background-position: 0 -380px; }
-.emoji-1F350 { background-position: -20px -380px; }
-.emoji-1F351 { background-position: -40px -380px; }
-.emoji-1F352 { background-position: -60px -380px; }
-.emoji-1F353 { background-position: -80px -380px; }
-.emoji-1F354 { background-position: -100px -380px; }
-.emoji-1F355 { background-position: -120px -380px; }
-.emoji-1F356 { background-position: -140px -380px; }
-.emoji-1F357 { background-position: -160px -380px; }
-.emoji-1F358 { background-position: -180px -380px; }
-.emoji-1F359 { background-position: -200px -380px; }
-.emoji-1F35A { background-position: -220px -380px; }
-.emoji-1F35B { background-position: -240px -380px; }
-.emoji-1F35C { background-position: -260px -380px; }
-.emoji-1F35D { background-position: -280px -380px; }
-.emoji-1F35E { background-position: -300px -380px; }
-.emoji-1F35F { background-position: -320px -380px; }
-.emoji-1F360 { background-position: -340px -380px; }
-.emoji-1F361 { background-position: -360px -380px; }
-.emoji-1F362 { background-position: -380px -380px; }
-.emoji-1F363 { background-position: -400px 0; }
-.emoji-1F364 { background-position: -400px -20px; }
-.emoji-1F365 { background-position: -400px -40px; }
-.emoji-1F366 { background-position: -400px -60px; }
-.emoji-1F367 { background-position: -400px -80px; }
-.emoji-1F368 { background-position: -400px -100px; }
-.emoji-1F369 { background-position: -400px -120px; }
-.emoji-1F36A { background-position: -400px -140px; }
-.emoji-1F36B { background-position: -400px -160px; }
-.emoji-1F36C { background-position: -400px -180px; }
-.emoji-1F36D { background-position: -400px -200px; }
-.emoji-1F36E { background-position: -400px -220px; }
-.emoji-1F36F { background-position: -400px -240px; }
-.emoji-1F370 { background-position: -400px -260px; }
-.emoji-1F371 { background-position: -400px -280px; }
-.emoji-1F372 { background-position: -400px -300px; }
-.emoji-1F373 { background-position: -400px -320px; }
-.emoji-1F374 { background-position: -400px -340px; }
-.emoji-1F375 { background-position: -400px -360px; }
-.emoji-1F376 { background-position: -400px -380px; }
-.emoji-1F377 { background-position: 0 -400px; }
-.emoji-1F378 { background-position: -20px -400px; }
-.emoji-1F379 { background-position: -40px -400px; }
-.emoji-1F37A { background-position: -60px -400px; }
-.emoji-1F37B { background-position: -80px -400px; }
-.emoji-1F37C { background-position: -100px -400px; }
-.emoji-1F37D { background-position: -120px -400px; }
-.emoji-1F37E { background-position: -140px -400px; }
-.emoji-1F37F { background-position: -160px -400px; }
-.emoji-1F380 { background-position: -180px -400px; }
-.emoji-1F381 { background-position: -200px -400px; }
-.emoji-1F382 { background-position: -220px -400px; }
-.emoji-1F383 { background-position: -240px -400px; }
-.emoji-1F384 { background-position: -260px -400px; }
-.emoji-1F385 { background-position: -280px -400px; }
-.emoji-1F385-1F3FB { background-position: -300px -400px; }
-.emoji-1F385-1F3FC { background-position: -320px -400px; }
-.emoji-1F385-1F3FD { background-position: -340px -400px; }
-.emoji-1F385-1F3FE { background-position: -360px -400px; }
-.emoji-1F385-1F3FF { background-position: -380px -400px; }
-.emoji-1F386 { background-position: -400px -400px; }
-.emoji-1F387 { background-position: -420px 0; }
-.emoji-1F388 { background-position: -420px -20px; }
-.emoji-1F389 { background-position: -420px -40px; }
-.emoji-1F38A { background-position: -420px -60px; }
-.emoji-1F38B { background-position: -420px -80px; }
-.emoji-1F38C { background-position: -420px -100px; }
-.emoji-1F38D { background-position: -420px -120px; }
-.emoji-1F38E { background-position: -420px -140px; }
-.emoji-1F38F { background-position: -420px -160px; }
-.emoji-1F390 { background-position: -420px -180px; }
-.emoji-1F391 { background-position: -420px -200px; }
-.emoji-1F392 { background-position: -420px -220px; }
-.emoji-1F393 { background-position: -420px -240px; }
-.emoji-1F396 { background-position: -420px -260px; }
-.emoji-1F397 { background-position: -420px -280px; }
-.emoji-1F399 { background-position: -420px -300px; }
-.emoji-1F39A { background-position: -420px -320px; }
-.emoji-1F39B { background-position: -420px -340px; }
-.emoji-1F39E { background-position: -420px -360px; }
-.emoji-1F39F { background-position: -420px -380px; }
-.emoji-1F3A0 { background-position: -420px -400px; }
-.emoji-1F3A1 { background-position: 0 -420px; }
-.emoji-1F3A2 { background-position: -20px -420px; }
-.emoji-1F3A3 { background-position: -40px -420px; }
-.emoji-1F3A4 { background-position: -60px -420px; }
-.emoji-1F3A5 { background-position: -80px -420px; }
-.emoji-1F3A6 { background-position: -100px -420px; }
-.emoji-1F3A7 { background-position: -120px -420px; }
-.emoji-1F3A8 { background-position: -140px -420px; }
-.emoji-1F3A9 { background-position: -160px -420px; }
-.emoji-1F3AA { background-position: -180px -420px; }
-.emoji-1F3AB { background-position: -200px -420px; }
-.emoji-1F3AC { background-position: -220px -420px; }
-.emoji-1F3AD { background-position: -240px -420px; }
-.emoji-1F3AE { background-position: -260px -420px; }
-.emoji-1F3AF { background-position: -280px -420px; }
-.emoji-1F3B0 { background-position: -300px -420px; }
-.emoji-1F3B1 { background-position: -320px -420px; }
-.emoji-1F3B2 { background-position: -340px -420px; }
-.emoji-1F3B3 { background-position: -360px -420px; }
-.emoji-1F3B4 { background-position: -380px -420px; }
-.emoji-1F3B5 { background-position: -400px -420px; }
-.emoji-1F3B6 { background-position: -420px -420px; }
-.emoji-1F3B7 { background-position: -440px 0; }
-.emoji-1F3B8 { background-position: -440px -20px; }
-.emoji-1F3B9 { background-position: -440px -40px; }
-.emoji-1F3BA { background-position: -440px -60px; }
-.emoji-1F3BB { background-position: -440px -80px; }
-.emoji-1F3BC { background-position: -440px -100px; }
-.emoji-1F3BD { background-position: -440px -120px; }
-.emoji-1F3BE { background-position: -440px -140px; }
-.emoji-1F3BF { background-position: -440px -160px; }
-.emoji-1F3C0 { background-position: -440px -180px; }
-.emoji-1F3C1 { background-position: -440px -200px; }
-.emoji-1F3C2 { background-position: -440px -220px; }
-.emoji-1F3C3 { background-position: -440px -240px; }
-.emoji-1F3C3-1F3FB { background-position: -440px -260px; }
-.emoji-1F3C3-1F3FC { background-position: -440px -280px; }
-.emoji-1F3C3-1F3FD { background-position: -440px -300px; }
-.emoji-1F3C3-1F3FE { background-position: -440px -320px; }
-.emoji-1F3C3-1F3FF { background-position: -440px -340px; }
-.emoji-1F3C4 { background-position: -440px -360px; }
-.emoji-1F3C4-1F3FB { background-position: -440px -380px; }
-.emoji-1F3C4-1F3FC { background-position: -440px -400px; }
-.emoji-1F3C4-1F3FD { background-position: -440px -420px; }
-.emoji-1F3C4-1F3FE { background-position: 0 -440px; }
-.emoji-1F3C4-1F3FF { background-position: -20px -440px; }
-.emoji-1F3C5 { background-position: -40px -440px; }
-.emoji-1F3C6 { background-position: -60px -440px; }
-.emoji-1F3C7 { background-position: -80px -440px; }
-.emoji-1F3C7-1F3FB { background-position: -100px -440px; }
-.emoji-1F3C7-1F3FC { background-position: -120px -440px; }
-.emoji-1F3C7-1F3FD { background-position: -140px -440px; }
-.emoji-1F3C7-1F3FE { background-position: -160px -440px; }
-.emoji-1F3C7-1F3FF { background-position: -180px -440px; }
-.emoji-1F3C8 { background-position: -200px -440px; }
-.emoji-1F3C9 { background-position: -220px -440px; }
-.emoji-1F3CA { background-position: -240px -440px; }
-.emoji-1F3CA-1F3FB { background-position: -260px -440px; }
-.emoji-1F3CA-1F3FC { background-position: -280px -440px; }
-.emoji-1F3CA-1F3FD { background-position: -300px -440px; }
-.emoji-1F3CA-1F3FE { background-position: -320px -440px; }
-.emoji-1F3CA-1F3FF { background-position: -340px -440px; }
-.emoji-1F3CB { background-position: -360px -440px; }
-.emoji-1F3CB-1F3FB { background-position: -380px -440px; }
-.emoji-1F3CB-1F3FC { background-position: -400px -440px; }
-.emoji-1F3CB-1F3FD { background-position: -420px -440px; }
-.emoji-1F3CB-1F3FE { background-position: -440px -440px; }
-.emoji-1F3CB-1F3FF { background-position: -460px 0; }
-.emoji-1F3CC { background-position: -460px -20px; }
-.emoji-1F3CD { background-position: -460px -40px; }
-.emoji-1F3CE { background-position: -460px -60px; }
-.emoji-1F3CF { background-position: -460px -80px; }
-.emoji-1F3D0 { background-position: -460px -100px; }
-.emoji-1F3D1 { background-position: -460px -120px; }
-.emoji-1F3D2 { background-position: -460px -140px; }
-.emoji-1F3D3 { background-position: -460px -160px; }
-.emoji-1F3D4 { background-position: -460px -180px; }
-.emoji-1F3D5 { background-position: -460px -200px; }
-.emoji-1F3D6 { background-position: -460px -220px; }
-.emoji-1F3D7 { background-position: -460px -240px; }
-.emoji-1F3D8 { background-position: -460px -260px; }
-.emoji-1F3D9 { background-position: -460px -280px; }
-.emoji-1F3DA { background-position: -460px -300px; }
-.emoji-1F3DB { background-position: -460px -320px; }
-.emoji-1F3DC { background-position: -460px -340px; }
-.emoji-1F3DD { background-position: -460px -360px; }
-.emoji-1F3DE { background-position: -460px -380px; }
-.emoji-1F3DF { background-position: -460px -400px; }
-.emoji-1F3E0 { background-position: -460px -420px; }
-.emoji-1F3E1 { background-position: -460px -440px; }
-.emoji-1F3E2 { background-position: 0 -460px; }
-.emoji-1F3E3 { background-position: -20px -460px; }
-.emoji-1F3E4 { background-position: -40px -460px; }
-.emoji-1F3E5 { background-position: -60px -460px; }
-.emoji-1F3E6 { background-position: -80px -460px; }
-.emoji-1F3E7 { background-position: -100px -460px; }
-.emoji-1F3E8 { background-position: -120px -460px; }
-.emoji-1F3E9 { background-position: -140px -460px; }
-.emoji-1F3EA { background-position: -160px -460px; }
-.emoji-1F3EB { background-position: -180px -460px; }
-.emoji-1F3EC { background-position: -200px -460px; }
-.emoji-1F3ED { background-position: -220px -460px; }
-.emoji-1F3EE { background-position: -240px -460px; }
-.emoji-1F3EF { background-position: -260px -460px; }
-.emoji-1F3F0 { background-position: -280px -460px; }
-.emoji-1F3F3 { background-position: -300px -460px; }
-.emoji-1F3F4 { background-position: -320px -460px; }
-.emoji-1F3F5 { background-position: -340px -460px; }
-.emoji-1F3F7 { background-position: -360px -460px; }
-.emoji-1F3F8 { background-position: -380px -460px; }
-.emoji-1F3F9 { background-position: -400px -460px; }
-.emoji-1F3FA { background-position: -420px -460px; }
-.emoji-1F3FB { background-position: -440px -460px; }
-.emoji-1F3FC { background-position: -460px -460px; }
-.emoji-1F3FD { background-position: -480px 0; }
-.emoji-1F3FE { background-position: -480px -20px; }
-.emoji-1F3FF { background-position: -480px -40px; }
-.emoji-1F400 { background-position: -480px -60px; }
-.emoji-1F401 { background-position: -480px -80px; }
-.emoji-1F402 { background-position: -480px -100px; }
-.emoji-1F403 { background-position: -480px -120px; }
-.emoji-1F404 { background-position: -480px -140px; }
-.emoji-1F405 { background-position: -480px -160px; }
-.emoji-1F406 { background-position: -480px -180px; }
-.emoji-1F407 { background-position: -480px -200px; }
-.emoji-1F408 { background-position: -480px -220px; }
-.emoji-1F409 { background-position: -480px -240px; }
-.emoji-1F40A { background-position: -480px -260px; }
-.emoji-1F40B { background-position: -480px -280px; }
-.emoji-1F40C { background-position: -480px -300px; }
-.emoji-1F40D { background-position: -480px -320px; }
-.emoji-1F40E { background-position: -480px -340px; }
-.emoji-1F40F { background-position: -480px -360px; }
-.emoji-1F410 { background-position: -480px -380px; }
-.emoji-1F411 { background-position: -480px -400px; }
-.emoji-1F412 { background-position: -480px -420px; }
-.emoji-1F413 { background-position: -480px -440px; }
-.emoji-1F414 { background-position: -480px -460px; }
-.emoji-1F415 { background-position: 0 -480px; }
-.emoji-1F416 { background-position: -20px -480px; }
-.emoji-1F417 { background-position: -40px -480px; }
-.emoji-1F418 { background-position: -60px -480px; }
-.emoji-1F419 { background-position: -80px -480px; }
-.emoji-1F41A { background-position: -100px -480px; }
-.emoji-1F41B { background-position: -120px -480px; }
-.emoji-1F41C { background-position: -140px -480px; }
-.emoji-1F41D { background-position: -160px -480px; }
-.emoji-1F41E { background-position: -180px -480px; }
-.emoji-1F41F { background-position: -200px -480px; }
-.emoji-1F420 { background-position: -220px -480px; }
-.emoji-1F421 { background-position: -240px -480px; }
-.emoji-1F422 { background-position: -260px -480px; }
-.emoji-1F423 { background-position: -280px -480px; }
-.emoji-1F424 { background-position: -300px -480px; }
-.emoji-1F425 { background-position: -320px -480px; }
-.emoji-1F426 { background-position: -340px -480px; }
-.emoji-1F427 { background-position: -360px -480px; }
-.emoji-1F428 { background-position: -380px -480px; }
-.emoji-1F429 { background-position: -400px -480px; }
-.emoji-1F42A { background-position: -420px -480px; }
-.emoji-1F42B { background-position: -440px -480px; }
-.emoji-1F42C { background-position: -460px -480px; }
-.emoji-1F42D { background-position: -480px -480px; }
-.emoji-1F42E { background-position: -500px 0; }
-.emoji-1F42F { background-position: -500px -20px; }
-.emoji-1F430 { background-position: -500px -40px; }
-.emoji-1F431 { background-position: -500px -60px; }
-.emoji-1F432 { background-position: -500px -80px; }
-.emoji-1F433 { background-position: -500px -100px; }
-.emoji-1F434 { background-position: -500px -120px; }
-.emoji-1F435 { background-position: -500px -140px; }
-.emoji-1F436 { background-position: -500px -160px; }
-.emoji-1F437 { background-position: -500px -180px; }
-.emoji-1F438 { background-position: -500px -200px; }
-.emoji-1F439 { background-position: -500px -220px; }
-.emoji-1F43A { background-position: -500px -240px; }
-.emoji-1F43B { background-position: -500px -260px; }
-.emoji-1F43C { background-position: -500px -280px; }
-.emoji-1F43D { background-position: -500px -300px; }
-.emoji-1F43E { background-position: -500px -320px; }
-.emoji-1F43F { background-position: -500px -340px; }
-.emoji-1F440 { background-position: -500px -360px; }
-.emoji-1F441 { background-position: -500px -380px; }
-.emoji-1F441-1F5E8 { background-position: -500px -400px; }
-.emoji-1F442 { background-position: -500px -420px; }
-.emoji-1F442-1F3FB { background-position: -500px -440px; }
-.emoji-1F442-1F3FC { background-position: -500px -460px; }
-.emoji-1F442-1F3FD { background-position: -500px -480px; }
-.emoji-1F442-1F3FE { background-position: 0 -500px; }
-.emoji-1F442-1F3FF { background-position: -20px -500px; }
-.emoji-1F443 { background-position: -40px -500px; }
-.emoji-1F443-1F3FB { background-position: -60px -500px; }
-.emoji-1F443-1F3FC { background-position: -80px -500px; }
-.emoji-1F443-1F3FD { background-position: -100px -500px; }
-.emoji-1F443-1F3FE { background-position: -120px -500px; }
-.emoji-1F443-1F3FF { background-position: -140px -500px; }
-.emoji-1F444 { background-position: -160px -500px; }
-.emoji-1F445 { background-position: -180px -500px; }
-.emoji-1F446 { background-position: -200px -500px; }
-.emoji-1F446-1F3FB { background-position: -220px -500px; }
-.emoji-1F446-1F3FC { background-position: -240px -500px; }
-.emoji-1F446-1F3FD { background-position: -260px -500px; }
-.emoji-1F446-1F3FE { background-position: -280px -500px; }
-.emoji-1F446-1F3FF { background-position: -300px -500px; }
-.emoji-1F447 { background-position: -320px -500px; }
-.emoji-1F447-1F3FB { background-position: -340px -500px; }
-.emoji-1F447-1F3FC { background-position: -360px -500px; }
-.emoji-1F447-1F3FD { background-position: -380px -500px; }
-.emoji-1F447-1F3FE { background-position: -400px -500px; }
-.emoji-1F447-1F3FF { background-position: -420px -500px; }
-.emoji-1F448 { background-position: -440px -500px; }
-.emoji-1F448-1F3FB { background-position: -460px -500px; }
-.emoji-1F448-1F3FC { background-position: -480px -500px; }
-.emoji-1F448-1F3FD { background-position: -500px -500px; }
-.emoji-1F448-1F3FE { background-position: -520px 0; }
-.emoji-1F448-1F3FF { background-position: -520px -20px; }
-.emoji-1F449 { background-position: -520px -40px; }
-.emoji-1F449-1F3FB { background-position: -520px -60px; }
-.emoji-1F449-1F3FC { background-position: -520px -80px; }
-.emoji-1F449-1F3FD { background-position: -520px -100px; }
-.emoji-1F449-1F3FE { background-position: -520px -120px; }
-.emoji-1F449-1F3FF { background-position: -520px -140px; }
-.emoji-1F44A { background-position: -520px -160px; }
-.emoji-1F44A-1F3FB { background-position: -520px -180px; }
-.emoji-1F44A-1F3FC { background-position: -520px -200px; }
-.emoji-1F44A-1F3FD { background-position: -520px -220px; }
-.emoji-1F44A-1F3FE { background-position: -520px -240px; }
-.emoji-1F44A-1F3FF { background-position: -520px -260px; }
-.emoji-1F44B { background-position: -520px -280px; }
-.emoji-1F44B-1F3FB { background-position: -520px -300px; }
-.emoji-1F44B-1F3FC { background-position: -520px -320px; }
-.emoji-1F44B-1F3FD { background-position: -520px -340px; }
-.emoji-1F44B-1F3FE { background-position: -520px -360px; }
-.emoji-1F44B-1F3FF { background-position: -520px -380px; }
-.emoji-1F44C { background-position: -520px -400px; }
-.emoji-1F44C-1F3FB { background-position: -520px -420px; }
-.emoji-1F44C-1F3FC { background-position: -520px -440px; }
-.emoji-1F44C-1F3FD { background-position: -520px -460px; }
-.emoji-1F44C-1F3FE { background-position: -520px -480px; }
-.emoji-1F44C-1F3FF { background-position: -520px -500px; }
-.emoji-1F44D { background-position: 0 -520px; }
-.emoji-1F44D-1F3FB { background-position: -20px -520px; }
-.emoji-1F44D-1F3FC { background-position: -40px -520px; }
-.emoji-1F44D-1F3FD { background-position: -60px -520px; }
-.emoji-1F44D-1F3FE { background-position: -80px -520px; }
-.emoji-1F44D-1F3FF { background-position: -100px -520px; }
-.emoji-1F44E { background-position: -120px -520px; }
-.emoji-1F44E-1F3FB { background-position: -140px -520px; }
-.emoji-1F44E-1F3FC { background-position: -160px -520px; }
-.emoji-1F44E-1F3FD { background-position: -180px -520px; }
-.emoji-1F44E-1F3FE { background-position: -200px -520px; }
-.emoji-1F44E-1F3FF { background-position: -220px -520px; }
-.emoji-1F44F { background-position: -240px -520px; }
-.emoji-1F44F-1F3FB { background-position: -260px -520px; }
-.emoji-1F44F-1F3FC { background-position: -280px -520px; }
-.emoji-1F44F-1F3FD { background-position: -300px -520px; }
-.emoji-1F44F-1F3FE { background-position: -320px -520px; }
-.emoji-1F44F-1F3FF { background-position: -340px -520px; }
-.emoji-1F450 { background-position: -360px -520px; }
-.emoji-1F450-1F3FB { background-position: -380px -520px; }
-.emoji-1F450-1F3FC { background-position: -400px -520px; }
-.emoji-1F450-1F3FD { background-position: -420px -520px; }
-.emoji-1F450-1F3FE { background-position: -440px -520px; }
-.emoji-1F450-1F3FF { background-position: -460px -520px; }
-.emoji-1F451 { background-position: -480px -520px; }
-.emoji-1F452 { background-position: -500px -520px; }
-.emoji-1F453 { background-position: -520px -520px; }
-.emoji-1F454 { background-position: -540px 0; }
-.emoji-1F455 { background-position: -540px -20px; }
-.emoji-1F456 { background-position: -540px -40px; }
-.emoji-1F457 { background-position: -540px -60px; }
-.emoji-1F458 { background-position: -540px -80px; }
-.emoji-1F459 { background-position: -540px -100px; }
-.emoji-1F45A { background-position: -540px -120px; }
-.emoji-1F45B { background-position: -540px -140px; }
-.emoji-1F45C { background-position: -540px -160px; }
-.emoji-1F45D { background-position: -540px -180px; }
-.emoji-1F45E { background-position: -540px -200px; }
-.emoji-1F45F { background-position: -540px -220px; }
-.emoji-1F460 { background-position: -540px -240px; }
-.emoji-1F461 { background-position: -540px -260px; }
-.emoji-1F462 { background-position: -540px -280px; }
-.emoji-1F463 { background-position: -540px -300px; }
-.emoji-1F464 { background-position: -540px -320px; }
-.emoji-1F465 { background-position: -540px -340px; }
-.emoji-1F466 { background-position: -540px -360px; }
-.emoji-1F466-1F3FB { background-position: -540px -380px; }
-.emoji-1F466-1F3FC { background-position: -540px -400px; }
-.emoji-1F466-1F3FD { background-position: -540px -420px; }
-.emoji-1F466-1F3FE { background-position: -540px -440px; }
-.emoji-1F466-1F3FF { background-position: -540px -460px; }
-.emoji-1F467 { background-position: -540px -480px; }
-.emoji-1F467-1F3FB { background-position: -540px -500px; }
-.emoji-1F467-1F3FC { background-position: -540px -520px; }
-.emoji-1F467-1F3FD { background-position: 0 -540px; }
-.emoji-1F467-1F3FE { background-position: -20px -540px; }
-.emoji-1F467-1F3FF { background-position: -40px -540px; }
-.emoji-1F468 { background-position: -60px -540px; }
-.emoji-1F468-1F3FB { background-position: -80px -540px; }
-.emoji-1F468-1F3FC { background-position: -100px -540px; }
-.emoji-1F468-1F3FD { background-position: -120px -540px; }
-.emoji-1F468-1F3FE { background-position: -140px -540px; }
-.emoji-1F468-1F3FF { background-position: -160px -540px; }
-.emoji-1F468-1F468-1F466 { background-position: -180px -540px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; }
-.emoji-1F468-1F468-1F467 { background-position: -220px -540px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; }
-.emoji-1F468-1F469-1F467 { background-position: -300px -540px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; }
-.emoji-1F468-2764-1F468 { background-position: -360px -540px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; }
-.emoji-1F469 { background-position: -400px -540px; }
-.emoji-1F469-1F3FB { background-position: -420px -540px; }
-.emoji-1F469-1F3FC { background-position: -440px -540px; }
-.emoji-1F469-1F3FD { background-position: -460px -540px; }
-.emoji-1F469-1F3FE { background-position: -480px -540px; }
-.emoji-1F469-1F3FF { background-position: -500px -540px; }
-.emoji-1F469-1F469-1F466 { background-position: -520px -540px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; }
-.emoji-1F469-1F469-1F467 { background-position: -560px 0; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; }
-.emoji-1F469-2764-1F469 { background-position: -560px -60px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; }
-.emoji-1F46A { background-position: -560px -100px; }
-.emoji-1F46B { background-position: -560px -120px; }
-.emoji-1F46C { background-position: -560px -140px; }
-.emoji-1F46D { background-position: -560px -160px; }
-.emoji-1F46E { background-position: -560px -180px; }
-.emoji-1F46E-1F3FB { background-position: -560px -200px; }
-.emoji-1F46E-1F3FC { background-position: -560px -220px; }
-.emoji-1F46E-1F3FD { background-position: -560px -240px; }
-.emoji-1F46E-1F3FE { background-position: -560px -260px; }
-.emoji-1F46E-1F3FF { background-position: -560px -280px; }
-.emoji-1F46F { background-position: -560px -300px; }
-.emoji-1F470 { background-position: -560px -320px; }
-.emoji-1F470-1F3FB { background-position: -560px -340px; }
-.emoji-1F470-1F3FC { background-position: -560px -360px; }
-.emoji-1F470-1F3FD { background-position: -560px -380px; }
-.emoji-1F470-1F3FE { background-position: -560px -400px; }
-.emoji-1F470-1F3FF { background-position: -560px -420px; }
-.emoji-1F471 { background-position: -560px -440px; }
-.emoji-1F471-1F3FB { background-position: -560px -460px; }
-.emoji-1F471-1F3FC { background-position: -560px -480px; }
-.emoji-1F471-1F3FD { background-position: -560px -500px; }
-.emoji-1F471-1F3FE { background-position: -560px -520px; }
-.emoji-1F471-1F3FF { background-position: -560px -540px; }
-.emoji-1F472 { background-position: 0 -560px; }
-.emoji-1F472-1F3FB { background-position: -20px -560px; }
-.emoji-1F472-1F3FC { background-position: -40px -560px; }
-.emoji-1F472-1F3FD { background-position: -60px -560px; }
-.emoji-1F472-1F3FE { background-position: -80px -560px; }
-.emoji-1F472-1F3FF { background-position: -100px -560px; }
-.emoji-1F473 { background-position: -120px -560px; }
-.emoji-1F473-1F3FB { background-position: -140px -560px; }
-.emoji-1F473-1F3FC { background-position: -160px -560px; }
-.emoji-1F473-1F3FD { background-position: -180px -560px; }
-.emoji-1F473-1F3FE { background-position: -200px -560px; }
-.emoji-1F473-1F3FF { background-position: -220px -560px; }
-.emoji-1F474 { background-position: -240px -560px; }
-.emoji-1F474-1F3FB { background-position: -260px -560px; }
-.emoji-1F474-1F3FC { background-position: -280px -560px; }
-.emoji-1F474-1F3FD { background-position: -300px -560px; }
-.emoji-1F474-1F3FE { background-position: -320px -560px; }
-.emoji-1F474-1F3FF { background-position: -340px -560px; }
-.emoji-1F475 { background-position: -360px -560px; }
-.emoji-1F475-1F3FB { background-position: -380px -560px; }
-.emoji-1F475-1F3FC { background-position: -400px -560px; }
-.emoji-1F475-1F3FD { background-position: -420px -560px; }
-.emoji-1F475-1F3FE { background-position: -440px -560px; }
-.emoji-1F475-1F3FF { background-position: -460px -560px; }
-.emoji-1F476 { background-position: -480px -560px; }
-.emoji-1F476-1F3FB { background-position: -500px -560px; }
-.emoji-1F476-1F3FC { background-position: -520px -560px; }
-.emoji-1F476-1F3FD { background-position: -540px -560px; }
-.emoji-1F476-1F3FE { background-position: -560px -560px; }
-.emoji-1F476-1F3FF { background-position: -580px 0; }
-.emoji-1F477 { background-position: -580px -20px; }
-.emoji-1F477-1F3FB { background-position: -580px -40px; }
-.emoji-1F477-1F3FC { background-position: -580px -60px; }
-.emoji-1F477-1F3FD { background-position: -580px -80px; }
-.emoji-1F477-1F3FE { background-position: -580px -100px; }
-.emoji-1F477-1F3FF { background-position: -580px -120px; }
-.emoji-1F478 { background-position: -580px -140px; }
-.emoji-1F478-1F3FB { background-position: -580px -160px; }
-.emoji-1F478-1F3FC { background-position: -580px -180px; }
-.emoji-1F478-1F3FD { background-position: -580px -200px; }
-.emoji-1F478-1F3FE { background-position: -580px -220px; }
-.emoji-1F478-1F3FF { background-position: -580px -240px; }
-.emoji-1F479 { background-position: -580px -260px; }
-.emoji-1F47A { background-position: -580px -280px; }
-.emoji-1F47B { background-position: -580px -300px; }
-.emoji-1F47C { background-position: -580px -320px; }
-.emoji-1F47C-1F3FB { background-position: -580px -340px; }
-.emoji-1F47C-1F3FC { background-position: -580px -360px; }
-.emoji-1F47C-1F3FD { background-position: -580px -380px; }
-.emoji-1F47C-1F3FE { background-position: -580px -400px; }
-.emoji-1F47C-1F3FF { background-position: -580px -420px; }
-.emoji-1F47D { background-position: -580px -440px; }
-.emoji-1F47E { background-position: -580px -460px; }
-.emoji-1F47F { background-position: -580px -480px; }
-.emoji-1F480 { background-position: -580px -500px; }
-.emoji-1F481 { background-position: -580px -520px; }
-.emoji-1F481-1F3FB { background-position: -580px -540px; }
-.emoji-1F481-1F3FC { background-position: -580px -560px; }
-.emoji-1F481-1F3FD { background-position: 0 -580px; }
-.emoji-1F481-1F3FE { background-position: -20px -580px; }
-.emoji-1F481-1F3FF { background-position: -40px -580px; }
-.emoji-1F482 { background-position: -60px -580px; }
-.emoji-1F482-1F3FB { background-position: -80px -580px; }
-.emoji-1F482-1F3FC { background-position: -100px -580px; }
-.emoji-1F482-1F3FD { background-position: -120px -580px; }
-.emoji-1F482-1F3FE { background-position: -140px -580px; }
-.emoji-1F482-1F3FF { background-position: -160px -580px; }
-.emoji-1F483 { background-position: -180px -580px; }
-.emoji-1F483-1F3FB { background-position: -200px -580px; }
-.emoji-1F483-1F3FC { background-position: -220px -580px; }
-.emoji-1F483-1F3FD { background-position: -240px -580px; }
-.emoji-1F483-1F3FE { background-position: -260px -580px; }
-.emoji-1F483-1F3FF { background-position: -280px -580px; }
-.emoji-1F484 { background-position: -300px -580px; }
-.emoji-1F485 { background-position: -320px -580px; }
-.emoji-1F485-1F3FB { background-position: -340px -580px; }
-.emoji-1F485-1F3FC { background-position: -360px -580px; }
-.emoji-1F485-1F3FD { background-position: -380px -580px; }
-.emoji-1F485-1F3FE { background-position: -400px -580px; }
-.emoji-1F485-1F3FF { background-position: -420px -580px; }
-.emoji-1F486 { background-position: -440px -580px; }
-.emoji-1F486-1F3FB { background-position: -460px -580px; }
-.emoji-1F486-1F3FC { background-position: -480px -580px; }
-.emoji-1F486-1F3FD { background-position: -500px -580px; }
-.emoji-1F486-1F3FE { background-position: -520px -580px; }
-.emoji-1F486-1F3FF { background-position: -540px -580px; }
-.emoji-1F487 { background-position: -560px -580px; }
-.emoji-1F487-1F3FB { background-position: -580px -580px; }
-.emoji-1F487-1F3FC { background-position: -600px 0; }
-.emoji-1F487-1F3FD { background-position: -600px -20px; }
-.emoji-1F487-1F3FE { background-position: -600px -40px; }
-.emoji-1F487-1F3FF { background-position: -600px -60px; }
-.emoji-1F488 { background-position: -600px -80px; }
-.emoji-1F489 { background-position: -600px -100px; }
-.emoji-1F48A { background-position: -600px -120px; }
-.emoji-1F48B { background-position: -600px -140px; }
-.emoji-1F48C { background-position: -600px -160px; }
-.emoji-1F48D { background-position: -600px -180px; }
-.emoji-1F48E { background-position: -600px -200px; }
-.emoji-1F48F { background-position: -600px -220px; }
-.emoji-1F490 { background-position: -600px -240px; }
-.emoji-1F491 { background-position: -600px -260px; }
-.emoji-1F492 { background-position: -600px -280px; }
-.emoji-1F493 { background-position: -600px -300px; }
-.emoji-1F494 { background-position: -600px -320px; }
-.emoji-1F495 { background-position: -600px -340px; }
-.emoji-1F496 { background-position: -600px -360px; }
-.emoji-1F497 { background-position: -600px -380px; }
-.emoji-1F498 { background-position: -600px -400px; }
-.emoji-1F499 { background-position: -600px -420px; }
-.emoji-1F49A { background-position: -600px -440px; }
-.emoji-1F49B { background-position: -600px -460px; }
-.emoji-1F49C { background-position: -600px -480px; }
-.emoji-1F49D { background-position: -600px -500px; }
-.emoji-1F49E { background-position: -600px -520px; }
-.emoji-1F49F { background-position: -600px -540px; }
-.emoji-1F4A0 { background-position: -600px -560px; }
-.emoji-1F4A1 { background-position: -600px -580px; }
-.emoji-1F4A2 { background-position: 0 -600px; }
-.emoji-1F4A3 { background-position: -20px -600px; }
-.emoji-1F4A4 { background-position: -40px -600px; }
-.emoji-1F4A5 { background-position: -60px -600px; }
-.emoji-1F4A6 { background-position: -80px -600px; }
-.emoji-1F4A7 { background-position: -100px -600px; }
-.emoji-1F4A8 { background-position: -120px -600px; }
-.emoji-1F4A9 { background-position: -140px -600px; }
-.emoji-1F4AA { background-position: -160px -600px; }
-.emoji-1F4AA-1F3FB { background-position: -180px -600px; }
-.emoji-1F4AA-1F3FC { background-position: -200px -600px; }
-.emoji-1F4AA-1F3FD { background-position: -220px -600px; }
-.emoji-1F4AA-1F3FE { background-position: -240px -600px; }
-.emoji-1F4AA-1F3FF { background-position: -260px -600px; }
-.emoji-1F4AB { background-position: -280px -600px; }
-.emoji-1F4AC { background-position: -300px -600px; }
-.emoji-1F4AD { background-position: -320px -600px; }
-.emoji-1F4AE { background-position: -340px -600px; }
-.emoji-1F4AF { background-position: -360px -600px; }
-.emoji-1F4B0 { background-position: -380px -600px; }
-.emoji-1F4B1 { background-position: -400px -600px; }
-.emoji-1F4B2 { background-position: -420px -600px; }
-.emoji-1F4B3 { background-position: -440px -600px; }
-.emoji-1F4B4 { background-position: -460px -600px; }
-.emoji-1F4B5 { background-position: -480px -600px; }
-.emoji-1F4B6 { background-position: -500px -600px; }
-.emoji-1F4B7 { background-position: -520px -600px; }
-.emoji-1F4B8 { background-position: -540px -600px; }
-.emoji-1F4B9 { background-position: -560px -600px; }
-.emoji-1F4BA { background-position: -580px -600px; }
-.emoji-1F4BB { background-position: -600px -600px; }
-.emoji-1F4BC { background-position: -620px 0; }
-.emoji-1F4BD { background-position: -620px -20px; }
-.emoji-1F4BE { background-position: -620px -40px; }
-.emoji-1F4BF { background-position: -620px -60px; }
-.emoji-1F4C0 { background-position: -620px -80px; }
-.emoji-1F4C1 { background-position: -620px -100px; }
-.emoji-1F4C2 { background-position: -620px -120px; }
-.emoji-1F4C3 { background-position: -620px -140px; }
-.emoji-1F4C4 { background-position: -620px -160px; }
-.emoji-1F4C5 { background-position: -620px -180px; }
-.emoji-1F4C6 { background-position: -620px -200px; }
-.emoji-1F4C7 { background-position: -620px -220px; }
-.emoji-1F4C8 { background-position: -620px -240px; }
-.emoji-1F4C9 { background-position: -620px -260px; }
-.emoji-1F4CA { background-position: -620px -280px; }
-.emoji-1F4CB { background-position: -620px -300px; }
-.emoji-1F4CC { background-position: -620px -320px; }
-.emoji-1F4CD { background-position: -620px -340px; }
-.emoji-1F4CE { background-position: -620px -360px; }
-.emoji-1F4CF { background-position: -620px -380px; }
-.emoji-1F4D0 { background-position: -620px -400px; }
-.emoji-1F4D1 { background-position: -620px -420px; }
-.emoji-1F4D2 { background-position: -620px -440px; }
-.emoji-1F4D3 { background-position: -620px -460px; }
-.emoji-1F4D4 { background-position: -620px -480px; }
-.emoji-1F4D5 { background-position: -620px -500px; }
-.emoji-1F4D6 { background-position: -620px -520px; }
-.emoji-1F4D7 { background-position: -620px -540px; }
-.emoji-1F4D8 { background-position: -620px -560px; }
-.emoji-1F4D9 { background-position: -620px -580px; }
-.emoji-1F4DA { background-position: -620px -600px; }
-.emoji-1F4DB { background-position: 0 -620px; }
-.emoji-1F4DC { background-position: -20px -620px; }
-.emoji-1F4DD { background-position: -40px -620px; }
-.emoji-1F4DE { background-position: -60px -620px; }
-.emoji-1F4DF { background-position: -80px -620px; }
-.emoji-1F4E0 { background-position: -100px -620px; }
-.emoji-1F4E1 { background-position: -120px -620px; }
-.emoji-1F4E2 { background-position: -140px -620px; }
-.emoji-1F4E3 { background-position: -160px -620px; }
-.emoji-1F4E4 { background-position: -180px -620px; }
-.emoji-1F4E5 { background-position: -200px -620px; }
-.emoji-1F4E6 { background-position: -220px -620px; }
-.emoji-1F4E7 { background-position: -240px -620px; }
-.emoji-1F4E8 { background-position: -260px -620px; }
-.emoji-1F4E9 { background-position: -280px -620px; }
-.emoji-1F4EA { background-position: -300px -620px; }
-.emoji-1F4EB { background-position: -320px -620px; }
-.emoji-1F4EC { background-position: -340px -620px; }
-.emoji-1F4ED { background-position: -360px -620px; }
-.emoji-1F4EE { background-position: -380px -620px; }
-.emoji-1F4EF { background-position: -400px -620px; }
-.emoji-1F4F0 { background-position: -420px -620px; }
-.emoji-1F4F1 { background-position: -440px -620px; }
-.emoji-1F4F2 { background-position: -460px -620px; }
-.emoji-1F4F3 { background-position: -480px -620px; }
-.emoji-1F4F4 { background-position: -500px -620px; }
-.emoji-1F4F5 { background-position: -520px -620px; }
-.emoji-1F4F6 { background-position: -540px -620px; }
-.emoji-1F4F7 { background-position: -560px -620px; }
-.emoji-1F4F8 { background-position: -580px -620px; }
-.emoji-1F4F9 { background-position: -600px -620px; }
-.emoji-1F4FA { background-position: -620px -620px; }
-.emoji-1F4FB { background-position: -640px 0; }
-.emoji-1F4FC { background-position: -640px -20px; }
-.emoji-1F4FD { background-position: -640px -40px; }
-.emoji-1F4FF { background-position: -640px -60px; }
-.emoji-1F500 { background-position: -640px -80px; }
-.emoji-1F501 { background-position: -640px -100px; }
-.emoji-1F502 { background-position: -640px -120px; }
-.emoji-1F503 { background-position: -640px -140px; }
-.emoji-1F504 { background-position: -640px -160px; }
-.emoji-1F505 { background-position: -640px -180px; }
-.emoji-1F506 { background-position: -640px -200px; }
-.emoji-1F507 { background-position: -640px -220px; }
-.emoji-1F508 { background-position: -640px -240px; }
-.emoji-1F509 { background-position: -640px -260px; }
-.emoji-1F50A { background-position: -640px -280px; }
-.emoji-1F50B { background-position: -640px -300px; }
-.emoji-1F50C { background-position: -640px -320px; }
-.emoji-1F50D { background-position: -640px -340px; }
-.emoji-1F50E { background-position: -640px -360px; }
-.emoji-1F50F { background-position: -640px -380px; }
-.emoji-1F510 { background-position: -640px -400px; }
-.emoji-1F511 { background-position: -640px -420px; }
-.emoji-1F512 { background-position: -640px -440px; }
-.emoji-1F513 { background-position: -640px -460px; }
-.emoji-1F514 { background-position: -640px -480px; }
-.emoji-1F515 { background-position: -640px -500px; }
-.emoji-1F516 { background-position: -640px -520px; }
-.emoji-1F517 { background-position: -640px -540px; }
-.emoji-1F518 { background-position: -640px -560px; }
-.emoji-1F519 { background-position: -640px -580px; }
-.emoji-1F51A { background-position: -640px -600px; }
-.emoji-1F51B { background-position: -640px -620px; }
-.emoji-1F51C { background-position: 0 -640px; }
-.emoji-1F51D { background-position: -20px -640px; }
-.emoji-1F51E { background-position: -40px -640px; }
-.emoji-1F51F { background-position: -60px -640px; }
-.emoji-1F520 { background-position: -80px -640px; }
-.emoji-1F521 { background-position: -100px -640px; }
-.emoji-1F522 { background-position: -120px -640px; }
-.emoji-1F523 { background-position: -140px -640px; }
-.emoji-1F524 { background-position: -160px -640px; }
-.emoji-1F525 { background-position: -180px -640px; }
-.emoji-1F526 { background-position: -200px -640px; }
-.emoji-1F527 { background-position: -220px -640px; }
-.emoji-1F528 { background-position: -240px -640px; }
-.emoji-1F529 { background-position: -260px -640px; }
-.emoji-1F52A { background-position: -280px -640px; }
-.emoji-1F52B { background-position: -300px -640px; }
-.emoji-1F52C { background-position: -320px -640px; }
-.emoji-1F52D { background-position: -340px -640px; }
-.emoji-1F52E { background-position: -360px -640px; }
-.emoji-1F52F { background-position: -380px -640px; }
-.emoji-1F530 { background-position: -400px -640px; }
-.emoji-1F531 { background-position: -420px -640px; }
-.emoji-1F532 { background-position: -440px -640px; }
-.emoji-1F533 { background-position: -460px -640px; }
-.emoji-1F534 { background-position: -480px -640px; }
-.emoji-1F535 { background-position: -500px -640px; }
-.emoji-1F536 { background-position: -520px -640px; }
-.emoji-1F537 { background-position: -540px -640px; }
-.emoji-1F538 { background-position: -560px -640px; }
-.emoji-1F539 { background-position: -580px -640px; }
-.emoji-1F53A { background-position: -600px -640px; }
-.emoji-1F53B { background-position: -620px -640px; }
-.emoji-1F53C { background-position: -640px -640px; }
-.emoji-1F53D { background-position: -660px 0; }
-.emoji-1F549 { background-position: -660px -20px; }
-.emoji-1F54A { background-position: -660px -40px; }
-.emoji-1F54B { background-position: -660px -60px; }
-.emoji-1F54C { background-position: -660px -80px; }
-.emoji-1F54D { background-position: -660px -100px; }
-.emoji-1F54E { background-position: -660px -120px; }
-.emoji-1F550 { background-position: -660px -140px; }
-.emoji-1F551 { background-position: -660px -160px; }
-.emoji-1F552 { background-position: -660px -180px; }
-.emoji-1F553 { background-position: -660px -200px; }
-.emoji-1F554 { background-position: -660px -220px; }
-.emoji-1F555 { background-position: -660px -240px; }
-.emoji-1F556 { background-position: -660px -260px; }
-.emoji-1F557 { background-position: -660px -280px; }
-.emoji-1F558 { background-position: -660px -300px; }
-.emoji-1F559 { background-position: -660px -320px; }
-.emoji-1F55A { background-position: -660px -340px; }
-.emoji-1F55B { background-position: -660px -360px; }
-.emoji-1F55C { background-position: -660px -380px; }
-.emoji-1F55D { background-position: -660px -400px; }
-.emoji-1F55E { background-position: -660px -420px; }
-.emoji-1F55F { background-position: -660px -440px; }
-.emoji-1F560 { background-position: -660px -460px; }
-.emoji-1F561 { background-position: -660px -480px; }
-.emoji-1F562 { background-position: -660px -500px; }
-.emoji-1F563 { background-position: -660px -520px; }
-.emoji-1F564 { background-position: -660px -540px; }
-.emoji-1F565 { background-position: -660px -560px; }
-.emoji-1F566 { background-position: -660px -580px; }
-.emoji-1F567 { background-position: -660px -600px; }
-.emoji-1F56F { background-position: -660px -620px; }
-.emoji-1F570 { background-position: -660px -640px; }
-.emoji-1F573 { background-position: 0 -660px; }
-.emoji-1F574 { background-position: -20px -660px; }
-.emoji-1F575 { background-position: -40px -660px; }
-.emoji-1F575-1F3FB { background-position: -60px -660px; }
-.emoji-1F575-1F3FC { background-position: -80px -660px; }
-.emoji-1F575-1F3FD { background-position: -100px -660px; }
-.emoji-1F575-1F3FE { background-position: -120px -660px; }
-.emoji-1F575-1F3FF { background-position: -140px -660px; }
-.emoji-1F576 { background-position: -160px -660px; }
-.emoji-1F577 { background-position: -180px -660px; }
-.emoji-1F578 { background-position: -200px -660px; }
-.emoji-1F579 { background-position: -220px -660px; }
-.emoji-1F57A { background-position: -240px -660px; }
-.emoji-1F57A-1F3FB { background-position: -260px -660px; }
-.emoji-1F57A-1F3FC { background-position: -280px -660px; }
-.emoji-1F57A-1F3FD { background-position: -300px -660px; }
-.emoji-1F57A-1F3FE { background-position: -320px -660px; }
-.emoji-1F57A-1F3FF { background-position: -340px -660px; }
-.emoji-1F587 { background-position: -360px -660px; }
-.emoji-1F58A { background-position: -380px -660px; }
-.emoji-1F58B { background-position: -400px -660px; }
-.emoji-1F58C { background-position: -420px -660px; }
-.emoji-1F58D { background-position: -440px -660px; }
-.emoji-1F590 { background-position: -460px -660px; }
-.emoji-1F590-1F3FB { background-position: -480px -660px; }
-.emoji-1F590-1F3FC { background-position: -500px -660px; }
-.emoji-1F590-1F3FD { background-position: -520px -660px; }
-.emoji-1F590-1F3FE { background-position: -540px -660px; }
-.emoji-1F590-1F3FF { background-position: -560px -660px; }
-.emoji-1F595 { background-position: -580px -660px; }
-.emoji-1F595-1F3FB { background-position: -600px -660px; }
-.emoji-1F595-1F3FC { background-position: -620px -660px; }
-.emoji-1F595-1F3FD { background-position: -640px -660px; }
-.emoji-1F595-1F3FE { background-position: -660px -660px; }
-.emoji-1F595-1F3FF { background-position: -680px 0; }
-.emoji-1F596 { background-position: -680px -20px; }
-.emoji-1F596-1F3FB { background-position: -680px -40px; }
-.emoji-1F596-1F3FC { background-position: -680px -60px; }
-.emoji-1F596-1F3FD { background-position: -680px -80px; }
-.emoji-1F596-1F3FE { background-position: -680px -100px; }
-.emoji-1F596-1F3FF { background-position: -680px -120px; }
-.emoji-1F5A4 { background-position: -680px -140px; }
-.emoji-1F5A5 { background-position: -680px -160px; }
-.emoji-1F5A8 { background-position: -680px -180px; }
-.emoji-1F5B1 { background-position: -680px -200px; }
-.emoji-1F5B2 { background-position: -680px -220px; }
-.emoji-1F5BC { background-position: -680px -240px; }
-.emoji-1F5C2 { background-position: -680px -260px; }
-.emoji-1F5C3 { background-position: -680px -280px; }
-.emoji-1F5C4 { background-position: -680px -300px; }
-.emoji-1F5D1 { background-position: -680px -320px; }
-.emoji-1F5D2 { background-position: -680px -340px; }
-.emoji-1F5D3 { background-position: -680px -360px; }
-.emoji-1F5DC { background-position: -680px -380px; }
-.emoji-1F5DD { background-position: -680px -400px; }
-.emoji-1F5DE { background-position: -680px -420px; }
-.emoji-1F5E1 { background-position: -680px -440px; }
-.emoji-1F5E3 { background-position: -680px -460px; }
-.emoji-1F5EF { background-position: -680px -480px; }
-.emoji-1F5F3 { background-position: -680px -500px; }
-.emoji-1F5FA { background-position: -680px -520px; }
-.emoji-1F5FB { background-position: -680px -540px; }
-.emoji-1F5FC { background-position: -680px -560px; }
-.emoji-1F5FD { background-position: -680px -580px; }
-.emoji-1F5FE { background-position: -680px -600px; }
-.emoji-1F5FF { background-position: -680px -620px; }
-.emoji-1F600 { background-position: -680px -640px; }
-.emoji-1F601 { background-position: -680px -660px; }
-.emoji-1F602 { background-position: 0 -680px; }
-.emoji-1F603 { background-position: -20px -680px; }
-.emoji-1F604 { background-position: -40px -680px; }
-.emoji-1F605 { background-position: -60px -680px; }
-.emoji-1F606 { background-position: -80px -680px; }
-.emoji-1F607 { background-position: -100px -680px; }
-.emoji-1F608 { background-position: -120px -680px; }
-.emoji-1F609 { background-position: -140px -680px; }
-.emoji-1F60A { background-position: -160px -680px; }
-.emoji-1F60B { background-position: -180px -680px; }
-.emoji-1F60C { background-position: -200px -680px; }
-.emoji-1F60D { background-position: -220px -680px; }
-.emoji-1F60E { background-position: -240px -680px; }
-.emoji-1F60F { background-position: -260px -680px; }
-.emoji-1F610 { background-position: -280px -680px; }
-.emoji-1F611 { background-position: -300px -680px; }
-.emoji-1F612 { background-position: -320px -680px; }
-.emoji-1F613 { background-position: -340px -680px; }
-.emoji-1F614 { background-position: -360px -680px; }
-.emoji-1F615 { background-position: -380px -680px; }
-.emoji-1F616 { background-position: -400px -680px; }
-.emoji-1F617 { background-position: -420px -680px; }
-.emoji-1F618 { background-position: -440px -680px; }
-.emoji-1F619 { background-position: -460px -680px; }
-.emoji-1F61A { background-position: -480px -680px; }
-.emoji-1F61B { background-position: -500px -680px; }
-.emoji-1F61C { background-position: -520px -680px; }
-.emoji-1F61D { background-position: -540px -680px; }
-.emoji-1F61E { background-position: -560px -680px; }
-.emoji-1F61F { background-position: -580px -680px; }
-.emoji-1F620 { background-position: -600px -680px; }
-.emoji-1F621 { background-position: -620px -680px; }
-.emoji-1F622 { background-position: -640px -680px; }
-.emoji-1F623 { background-position: -660px -680px; }
-.emoji-1F624 { background-position: -680px -680px; }
-.emoji-1F625 { background-position: -700px 0; }
-.emoji-1F626 { background-position: -700px -20px; }
-.emoji-1F627 { background-position: -700px -40px; }
-.emoji-1F628 { background-position: -700px -60px; }
-.emoji-1F629 { background-position: -700px -80px; }
-.emoji-1F62A { background-position: -700px -100px; }
-.emoji-1F62B { background-position: -700px -120px; }
-.emoji-1F62C { background-position: -700px -140px; }
-.emoji-1F62D { background-position: -700px -160px; }
-.emoji-1F62E { background-position: -700px -180px; }
-.emoji-1F62F { background-position: -700px -200px; }
-.emoji-1F630 { background-position: -700px -220px; }
-.emoji-1F631 { background-position: -700px -240px; }
-.emoji-1F632 { background-position: -700px -260px; }
-.emoji-1F633 { background-position: -700px -280px; }
-.emoji-1F634 { background-position: -700px -300px; }
-.emoji-1F635 { background-position: -700px -320px; }
-.emoji-1F636 { background-position: -700px -340px; }
-.emoji-1F637 { background-position: -700px -360px; }
-.emoji-1F638 { background-position: -700px -380px; }
-.emoji-1F639 { background-position: -700px -400px; }
-.emoji-1F63A { background-position: -700px -420px; }
-.emoji-1F63B { background-position: -700px -440px; }
-.emoji-1F63C { background-position: -700px -460px; }
-.emoji-1F63D { background-position: -700px -480px; }
-.emoji-1F63E { background-position: -700px -500px; }
-.emoji-1F63F { background-position: -700px -520px; }
-.emoji-1F640 { background-position: -700px -540px; }
-.emoji-1F641 { background-position: -700px -560px; }
-.emoji-1F642 { background-position: -700px -580px; }
-.emoji-1F643 { background-position: -700px -600px; }
-.emoji-1F644 { background-position: -700px -620px; }
-.emoji-1F645 { background-position: -700px -640px; }
-.emoji-1F645-1F3FB { background-position: -700px -660px; }
-.emoji-1F645-1F3FC { background-position: -700px -680px; }
-.emoji-1F645-1F3FD { background-position: 0 -700px; }
-.emoji-1F645-1F3FE { background-position: -20px -700px; }
-.emoji-1F645-1F3FF { background-position: -40px -700px; }
-.emoji-1F646 { background-position: -60px -700px; }
-.emoji-1F646-1F3FB { background-position: -80px -700px; }
-.emoji-1F646-1F3FC { background-position: -100px -700px; }
-.emoji-1F646-1F3FD { background-position: -120px -700px; }
-.emoji-1F646-1F3FE { background-position: -140px -700px; }
-.emoji-1F646-1F3FF { background-position: -160px -700px; }
-.emoji-1F647 { background-position: -180px -700px; }
-.emoji-1F647-1F3FB { background-position: -200px -700px; }
-.emoji-1F647-1F3FC { background-position: -220px -700px; }
-.emoji-1F647-1F3FD { background-position: -240px -700px; }
-.emoji-1F647-1F3FE { background-position: -260px -700px; }
-.emoji-1F647-1F3FF { background-position: -280px -700px; }
-.emoji-1F648 { background-position: -300px -700px; }
-.emoji-1F649 { background-position: -320px -700px; }
-.emoji-1F64A { background-position: -340px -700px; }
-.emoji-1F64B { background-position: -360px -700px; }
-.emoji-1F64B-1F3FB { background-position: -380px -700px; }
-.emoji-1F64B-1F3FC { background-position: -400px -700px; }
-.emoji-1F64B-1F3FD { background-position: -420px -700px; }
-.emoji-1F64B-1F3FE { background-position: -440px -700px; }
-.emoji-1F64B-1F3FF { background-position: -460px -700px; }
-.emoji-1F64C { background-position: -480px -700px; }
-.emoji-1F64C-1F3FB { background-position: -500px -700px; }
-.emoji-1F64C-1F3FC { background-position: -520px -700px; }
-.emoji-1F64C-1F3FD { background-position: -540px -700px; }
-.emoji-1F64C-1F3FE { background-position: -560px -700px; }
-.emoji-1F64C-1F3FF { background-position: -580px -700px; }
-.emoji-1F64D { background-position: -600px -700px; }
-.emoji-1F64D-1F3FB { background-position: -620px -700px; }
-.emoji-1F64D-1F3FC { background-position: -640px -700px; }
-.emoji-1F64D-1F3FD { background-position: -660px -700px; }
-.emoji-1F64D-1F3FE { background-position: -680px -700px; }
-.emoji-1F64D-1F3FF { background-position: -700px -700px; }
-.emoji-1F64E { background-position: -720px 0; }
-.emoji-1F64E-1F3FB { background-position: -720px -20px; }
-.emoji-1F64E-1F3FC { background-position: -720px -40px; }
-.emoji-1F64E-1F3FD { background-position: -720px -60px; }
-.emoji-1F64E-1F3FE { background-position: -720px -80px; }
-.emoji-1F64E-1F3FF { background-position: -720px -100px; }
-.emoji-1F64F { background-position: -720px -120px; }
-.emoji-1F64F-1F3FB { background-position: -720px -140px; }
-.emoji-1F64F-1F3FC { background-position: -720px -160px; }
-.emoji-1F64F-1F3FD { background-position: -720px -180px; }
-.emoji-1F64F-1F3FE { background-position: -720px -200px; }
-.emoji-1F64F-1F3FF { background-position: -720px -220px; }
-.emoji-1F680 { background-position: -720px -240px; }
-.emoji-1F681 { background-position: -720px -260px; }
-.emoji-1F682 { background-position: -720px -280px; }
-.emoji-1F683 { background-position: -720px -300px; }
-.emoji-1F684 { background-position: -720px -320px; }
-.emoji-1F685 { background-position: -720px -340px; }
-.emoji-1F686 { background-position: -720px -360px; }
-.emoji-1F687 { background-position: -720px -380px; }
-.emoji-1F688 { background-position: -720px -400px; }
-.emoji-1F689 { background-position: -720px -420px; }
-.emoji-1F68A { background-position: -720px -440px; }
-.emoji-1F68B { background-position: -720px -460px; }
-.emoji-1F68C { background-position: -720px -480px; }
-.emoji-1F68D { background-position: -720px -500px; }
-.emoji-1F68E { background-position: -720px -520px; }
-.emoji-1F68F { background-position: -720px -540px; }
-.emoji-1F690 { background-position: -720px -560px; }
-.emoji-1F691 { background-position: -720px -580px; }
-.emoji-1F692 { background-position: -720px -600px; }
-.emoji-1F693 { background-position: -720px -620px; }
-.emoji-1F694 { background-position: -720px -640px; }
-.emoji-1F695 { background-position: -720px -660px; }
-.emoji-1F696 { background-position: -720px -680px; }
-.emoji-1F697 { background-position: -720px -700px; }
-.emoji-1F698 { background-position: 0 -720px; }
-.emoji-1F699 { background-position: -20px -720px; }
-.emoji-1F69A { background-position: -40px -720px; }
-.emoji-1F69B { background-position: -60px -720px; }
-.emoji-1F69C { background-position: -80px -720px; }
-.emoji-1F69D { background-position: -100px -720px; }
-.emoji-1F69E { background-position: -120px -720px; }
-.emoji-1F69F { background-position: -140px -720px; }
-.emoji-1F6A0 { background-position: -160px -720px; }
-.emoji-1F6A1 { background-position: -180px -720px; }
-.emoji-1F6A2 { background-position: -200px -720px; }
-.emoji-1F6A3 { background-position: -220px -720px; }
-.emoji-1F6A3-1F3FB { background-position: -240px -720px; }
-.emoji-1F6A3-1F3FC { background-position: -260px -720px; }
-.emoji-1F6A3-1F3FD { background-position: -280px -720px; }
-.emoji-1F6A3-1F3FE { background-position: -300px -720px; }
-.emoji-1F6A3-1F3FF { background-position: -320px -720px; }
-.emoji-1F6A4 { background-position: -340px -720px; }
-.emoji-1F6A5 { background-position: -360px -720px; }
-.emoji-1F6A6 { background-position: -380px -720px; }
-.emoji-1F6A7 { background-position: -400px -720px; }
-.emoji-1F6A8 { background-position: -420px -720px; }
-.emoji-1F6A9 { background-position: -440px -720px; }
-.emoji-1F6AA { background-position: -460px -720px; }
-.emoji-1F6AB { background-position: -480px -720px; }
-.emoji-1F6AC { background-position: -500px -720px; }
-.emoji-1F6AD { background-position: -520px -720px; }
-.emoji-1F6AE { background-position: -540px -720px; }
-.emoji-1F6AF { background-position: -560px -720px; }
-.emoji-1F6B0 { background-position: -580px -720px; }
-.emoji-1F6B1 { background-position: -600px -720px; }
-.emoji-1F6B2 { background-position: -620px -720px; }
-.emoji-1F6B3 { background-position: -640px -720px; }
-.emoji-1F6B4 { background-position: -660px -720px; }
-.emoji-1F6B4-1F3FB { background-position: -680px -720px; }
-.emoji-1F6B4-1F3FC { background-position: -700px -720px; }
-.emoji-1F6B4-1F3FD { background-position: -720px -720px; }
-.emoji-1F6B4-1F3FE { background-position: -740px 0; }
-.emoji-1F6B4-1F3FF { background-position: -740px -20px; }
-.emoji-1F6B5 { background-position: -740px -40px; }
-.emoji-1F6B5-1F3FB { background-position: -740px -60px; }
-.emoji-1F6B5-1F3FC { background-position: -740px -80px; }
-.emoji-1F6B5-1F3FD { background-position: -740px -100px; }
-.emoji-1F6B5-1F3FE { background-position: -740px -120px; }
-.emoji-1F6B5-1F3FF { background-position: -740px -140px; }
-.emoji-1F6B6 { background-position: -740px -160px; }
-.emoji-1F6B6-1F3FB { background-position: -740px -180px; }
-.emoji-1F6B6-1F3FC { background-position: -740px -200px; }
-.emoji-1F6B6-1F3FD { background-position: -740px -220px; }
-.emoji-1F6B6-1F3FE { background-position: -740px -240px; }
-.emoji-1F6B6-1F3FF { background-position: -740px -260px; }
-.emoji-1F6B7 { background-position: -740px -280px; }
-.emoji-1F6B8 { background-position: -740px -300px; }
-.emoji-1F6B9 { background-position: -740px -320px; }
-.emoji-1F6BA { background-position: -740px -340px; }
-.emoji-1F6BB { background-position: -740px -360px; }
-.emoji-1F6BC { background-position: -740px -380px; }
-.emoji-1F6BD { background-position: -740px -400px; }
-.emoji-1F6BE { background-position: -740px -420px; }
-.emoji-1F6BF { background-position: -740px -440px; }
-.emoji-1F6C0 { background-position: -740px -460px; }
-.emoji-1F6C0-1F3FB { background-position: -740px -480px; }
-.emoji-1F6C0-1F3FC { background-position: -740px -500px; }
-.emoji-1F6C0-1F3FD { background-position: -740px -520px; }
-.emoji-1F6C0-1F3FE { background-position: -740px -540px; }
-.emoji-1F6C0-1F3FF { background-position: -740px -560px; }
-.emoji-1F6C1 { background-position: -740px -580px; }
-.emoji-1F6C2 { background-position: -740px -600px; }
-.emoji-1F6C3 { background-position: -740px -620px; }
-.emoji-1F6C4 { background-position: -740px -640px; }
-.emoji-1F6C5 { background-position: -740px -660px; }
-.emoji-1F6CB { background-position: -740px -680px; }
-.emoji-1F6CC { background-position: -740px -700px; }
-.emoji-1F6CD { background-position: -740px -720px; }
-.emoji-1F6CE { background-position: 0 -740px; }
-.emoji-1F6CF { background-position: -20px -740px; }
-.emoji-1F6D0 { background-position: -40px -740px; }
-.emoji-1F6D1 { background-position: -60px -740px; }
-.emoji-1F6D2 { background-position: -80px -740px; }
-.emoji-1F6E0 { background-position: -100px -740px; }
-.emoji-1F6E1 { background-position: -120px -740px; }
-.emoji-1F6E2 { background-position: -140px -740px; }
-.emoji-1F6E3 { background-position: -160px -740px; }
-.emoji-1F6E4 { background-position: -180px -740px; }
-.emoji-1F6E5 { background-position: -200px -740px; }
-.emoji-1F6E9 { background-position: -220px -740px; }
-.emoji-1F6EB { background-position: -240px -740px; }
-.emoji-1F6EC { background-position: -260px -740px; }
-.emoji-1F6F0 { background-position: -280px -740px; }
-.emoji-1F6F3 { background-position: -300px -740px; }
-.emoji-1F6F4 { background-position: -320px -740px; }
-.emoji-1F6F5 { background-position: -340px -740px; }
-.emoji-1F6F6 { background-position: -360px -740px; }
-.emoji-1F910 { background-position: -380px -740px; }
-.emoji-1F911 { background-position: -400px -740px; }
-.emoji-1F912 { background-position: -420px -740px; }
-.emoji-1F913 { background-position: -440px -740px; }
-.emoji-1F914 { background-position: -460px -740px; }
-.emoji-1F915 { background-position: -480px -740px; }
-.emoji-1F916 { background-position: -500px -740px; }
-.emoji-1F917 { background-position: -520px -740px; }
-.emoji-1F918 { background-position: -540px -740px; }
-.emoji-1F918-1F3FB { background-position: -560px -740px; }
-.emoji-1F918-1F3FC { background-position: -580px -740px; }
-.emoji-1F918-1F3FD { background-position: -600px -740px; }
-.emoji-1F918-1F3FE { background-position: -620px -740px; }
-.emoji-1F918-1F3FF { background-position: -640px -740px; }
-.emoji-1F919 { background-position: -660px -740px; }
-.emoji-1F919-1F3FB { background-position: -680px -740px; }
-.emoji-1F919-1F3FC { background-position: -700px -740px; }
-.emoji-1F919-1F3FD { background-position: -720px -740px; }
-.emoji-1F919-1F3FE { background-position: -740px -740px; }
-.emoji-1F919-1F3FF { background-position: -760px 0; }
-.emoji-1F91A { background-position: -760px -20px; }
-.emoji-1F91A-1F3FB { background-position: -760px -40px; }
-.emoji-1F91A-1F3FC { background-position: -760px -60px; }
-.emoji-1F91A-1F3FD { background-position: -760px -80px; }
-.emoji-1F91A-1F3FE { background-position: -760px -100px; }
-.emoji-1F91A-1F3FF { background-position: -760px -120px; }
-.emoji-1F91B { background-position: -760px -140px; }
-.emoji-1F91B-1F3FB { background-position: -760px -160px; }
-.emoji-1F91B-1F3FC { background-position: -760px -180px; }
-.emoji-1F91B-1F3FD { background-position: -760px -200px; }
-.emoji-1F91B-1F3FE { background-position: -760px -220px; }
-.emoji-1F91B-1F3FF { background-position: -760px -240px; }
-.emoji-1F91C { background-position: -760px -260px; }
-.emoji-1F91C-1F3FB { background-position: -760px -280px; }
-.emoji-1F91C-1F3FC { background-position: -760px -300px; }
-.emoji-1F91C-1F3FD { background-position: -760px -320px; }
-.emoji-1F91C-1F3FE { background-position: -760px -340px; }
-.emoji-1F91C-1F3FF { background-position: -760px -360px; }
-.emoji-1F91D { background-position: -760px -380px; }
-.emoji-1F91D-1F3FB { background-position: -760px -400px; }
-.emoji-1F91D-1F3FC { background-position: -760px -420px; }
-.emoji-1F91D-1F3FD { background-position: -760px -440px; }
-.emoji-1F91D-1F3FE { background-position: -760px -460px; }
-.emoji-1F91D-1F3FF { background-position: -760px -480px; }
-.emoji-1F91E { background-position: -760px -500px; }
-.emoji-1F91E-1F3FB { background-position: -760px -520px; }
-.emoji-1F91E-1F3FC { background-position: -760px -540px; }
-.emoji-1F91E-1F3FD { background-position: -760px -560px; }
-.emoji-1F91E-1F3FE { background-position: -760px -580px; }
-.emoji-1F91E-1F3FF { background-position: -760px -600px; }
-.emoji-1F920 { background-position: -760px -620px; }
-.emoji-1F921 { background-position: -760px -640px; }
-.emoji-1F922 { background-position: -760px -660px; }
-.emoji-1F923 { background-position: -760px -680px; }
-.emoji-1F924 { background-position: -760px -700px; }
-.emoji-1F925 { background-position: -760px -720px; }
-.emoji-1F926 { background-position: -760px -740px; }
-.emoji-1F926-1F3FB { background-position: 0 -760px; }
-.emoji-1F926-1F3FC { background-position: -20px -760px; }
-.emoji-1F926-1F3FD { background-position: -40px -760px; }
-.emoji-1F926-1F3FE { background-position: -60px -760px; }
-.emoji-1F926-1F3FF { background-position: -80px -760px; }
-.emoji-1F927 { background-position: -100px -760px; }
-.emoji-1F930 { background-position: -120px -760px; }
-.emoji-1F930-1F3FB { background-position: -140px -760px; }
-.emoji-1F930-1F3FC { background-position: -160px -760px; }
-.emoji-1F930-1F3FD { background-position: -180px -760px; }
-.emoji-1F930-1F3FE { background-position: -200px -760px; }
-.emoji-1F930-1F3FF { background-position: -220px -760px; }
-.emoji-1F933 { background-position: -240px -760px; }
-.emoji-1F933-1F3FB { background-position: -260px -760px; }
-.emoji-1F933-1F3FC { background-position: -280px -760px; }
-.emoji-1F933-1F3FD { background-position: -300px -760px; }
-.emoji-1F933-1F3FE { background-position: -320px -760px; }
-.emoji-1F933-1F3FF { background-position: -340px -760px; }
-.emoji-1F934 { background-position: -360px -760px; }
-.emoji-1F934-1F3FB { background-position: -380px -760px; }
-.emoji-1F934-1F3FC { background-position: -400px -760px; }
-.emoji-1F934-1F3FD { background-position: -420px -760px; }
-.emoji-1F934-1F3FE { background-position: -440px -760px; }
-.emoji-1F934-1F3FF { background-position: -460px -760px; }
-.emoji-1F935 { background-position: -480px -760px; }
-.emoji-1F935-1F3FB { background-position: -500px -760px; }
-.emoji-1F935-1F3FC { background-position: -520px -760px; }
-.emoji-1F935-1F3FD { background-position: -540px -760px; }
-.emoji-1F935-1F3FE { background-position: -560px -760px; }
-.emoji-1F935-1F3FF { background-position: -580px -760px; }
-.emoji-1F936 { background-position: -600px -760px; }
-.emoji-1F936-1F3FB { background-position: -620px -760px; }
-.emoji-1F936-1F3FC { background-position: -640px -760px; }
-.emoji-1F936-1F3FD { background-position: -660px -760px; }
-.emoji-1F936-1F3FE { background-position: -680px -760px; }
-.emoji-1F936-1F3FF { background-position: -700px -760px; }
-.emoji-1F937 { background-position: -720px -760px; }
-.emoji-1F937-1F3FB { background-position: -740px -760px; }
-.emoji-1F937-1F3FC { background-position: -760px -760px; }
-.emoji-1F937-1F3FD { background-position: -780px 0; }
-.emoji-1F937-1F3FE { background-position: -780px -20px; }
-.emoji-1F937-1F3FF { background-position: -780px -40px; }
-.emoji-1F938 { background-position: -780px -60px; }
-.emoji-1F938-1F3FB { background-position: -780px -80px; }
-.emoji-1F938-1F3FC { background-position: -780px -100px; }
-.emoji-1F938-1F3FD { background-position: -780px -120px; }
-.emoji-1F938-1F3FE { background-position: -780px -140px; }
-.emoji-1F938-1F3FF { background-position: -780px -160px; }
-.emoji-1F939 { background-position: -780px -180px; }
-.emoji-1F939-1F3FB { background-position: -780px -200px; }
-.emoji-1F939-1F3FC { background-position: -780px -220px; }
-.emoji-1F939-1F3FD { background-position: -780px -240px; }
-.emoji-1F939-1F3FE { background-position: -780px -260px; }
-.emoji-1F939-1F3FF { background-position: -780px -280px; }
-.emoji-1F93A { background-position: -780px -300px; }
-.emoji-1F93C { background-position: -780px -320px; }
-.emoji-1F93C-1F3FB { background-position: -780px -340px; }
-.emoji-1F93C-1F3FC { background-position: -780px -360px; }
-.emoji-1F93C-1F3FD { background-position: -780px -380px; }
-.emoji-1F93C-1F3FE { background-position: -780px -400px; }
-.emoji-1F93C-1F3FF { background-position: -780px -420px; }
-.emoji-1F93D { background-position: -780px -440px; }
-.emoji-1F93D-1F3FB { background-position: -780px -460px; }
-.emoji-1F93D-1F3FC { background-position: -780px -480px; }
-.emoji-1F93D-1F3FD { background-position: -780px -500px; }
-.emoji-1F93D-1F3FE { background-position: -780px -520px; }
-.emoji-1F93D-1F3FF { background-position: -780px -540px; }
-.emoji-1F93E { background-position: -780px -560px; }
-.emoji-1F93E-1F3FB { background-position: -780px -580px; }
-.emoji-1F93E-1F3FC { background-position: -780px -600px; }
-.emoji-1F93E-1F3FD { background-position: -780px -620px; }
-.emoji-1F93E-1F3FE { background-position: -780px -640px; }
-.emoji-1F93E-1F3FF { background-position: -780px -660px; }
-.emoji-1F940 { background-position: -780px -680px; }
-.emoji-1F941 { background-position: -780px -700px; }
-.emoji-1F942 { background-position: -780px -720px; }
-.emoji-1F943 { background-position: -780px -740px; }
-.emoji-1F944 { background-position: -780px -760px; }
-.emoji-1F945 { background-position: 0 -780px; }
-.emoji-1F947 { background-position: -20px -780px; }
-.emoji-1F948 { background-position: -40px -780px; }
-.emoji-1F949 { background-position: -60px -780px; }
-.emoji-1F94A { background-position: -80px -780px; }
-.emoji-1F94B { background-position: -100px -780px; }
-.emoji-1F950 { background-position: -120px -780px; }
-.emoji-1F951 { background-position: -140px -780px; }
-.emoji-1F952 { background-position: -160px -780px; }
-.emoji-1F953 { background-position: -180px -780px; }
-.emoji-1F954 { background-position: -200px -780px; }
-.emoji-1F955 { background-position: -220px -780px; }
-.emoji-1F956 { background-position: -240px -780px; }
-.emoji-1F957 { background-position: -260px -780px; }
-.emoji-1F958 { background-position: -280px -780px; }
-.emoji-1F959 { background-position: -300px -780px; }
-.emoji-1F95A { background-position: -320px -780px; }
-.emoji-1F95B { background-position: -340px -780px; }
-.emoji-1F95C { background-position: -360px -780px; }
-.emoji-1F95D { background-position: -380px -780px; }
-.emoji-1F95E { background-position: -400px -780px; }
-.emoji-1F980 { background-position: -420px -780px; }
-.emoji-1F981 { background-position: -440px -780px; }
-.emoji-1F982 { background-position: -460px -780px; }
-.emoji-1F983 { background-position: -480px -780px; }
-.emoji-1F984 { background-position: -500px -780px; }
-.emoji-1F985 { background-position: -520px -780px; }
-.emoji-1F986 { background-position: -540px -780px; }
-.emoji-1F987 { background-position: -560px -780px; }
-.emoji-1F988 { background-position: -580px -780px; }
-.emoji-1F989 { background-position: -600px -780px; }
-.emoji-1F98A { background-position: -620px -780px; }
-.emoji-1F98B { background-position: -640px -780px; }
-.emoji-1F98C { background-position: -660px -780px; }
-.emoji-1F98D { background-position: -680px -780px; }
-.emoji-1F98E { background-position: -700px -780px; }
-.emoji-1F98F { background-position: -720px -780px; }
-.emoji-1F990 { background-position: -740px -780px; }
-.emoji-1F991 { background-position: -760px -780px; }
-.emoji-1F9C0 { background-position: -780px -780px; }
-.emoji-203C { background-position: -800px 0; }
-.emoji-2049 { background-position: -800px -20px; }
-.emoji-2122 { background-position: -800px -40px; }
-.emoji-2139 { background-position: -800px -60px; }
-.emoji-2194 { background-position: -800px -80px; }
-.emoji-2195 { background-position: -800px -100px; }
-.emoji-2196 { background-position: -800px -120px; }
-.emoji-2197 { background-position: -800px -140px; }
-.emoji-2198 { background-position: -800px -160px; }
-.emoji-2199 { background-position: -800px -180px; }
-.emoji-21A9 { background-position: -800px -200px; }
-.emoji-21AA { background-position: -800px -220px; }
-.emoji-231A { background-position: -800px -240px; }
-.emoji-231B { background-position: -800px -260px; }
-.emoji-2328 { background-position: -800px -280px; }
-.emoji-23CF { background-position: -800px -300px; }
-.emoji-23E9 { background-position: -800px -320px; }
-.emoji-23EA { background-position: -800px -340px; }
-.emoji-23EB { background-position: -800px -360px; }
-.emoji-23EC { background-position: -800px -380px; }
-.emoji-23ED { background-position: -800px -400px; }
-.emoji-23EE { background-position: -800px -420px; }
-.emoji-23EF { background-position: -800px -440px; }
-.emoji-23F0 { background-position: -800px -460px; }
-.emoji-23F1 { background-position: -800px -480px; }
-.emoji-23F2 { background-position: -800px -500px; }
-.emoji-23F3 { background-position: -800px -520px; }
-.emoji-23F8 { background-position: -800px -540px; }
-.emoji-23F9 { background-position: -800px -560px; }
-.emoji-23FA { background-position: -800px -580px; }
-.emoji-24C2 { background-position: -800px -600px; }
-.emoji-25AA { background-position: -800px -620px; }
-.emoji-25AB { background-position: -800px -640px; }
-.emoji-25B6 { background-position: -800px -660px; }
-.emoji-25C0 { background-position: -800px -680px; }
-.emoji-25FB { background-position: -800px -700px; }
-.emoji-25FC { background-position: -800px -720px; }
-.emoji-25FD { background-position: -800px -740px; }
-.emoji-25FE { background-position: -800px -760px; }
-.emoji-2600 { background-position: -800px -780px; }
-.emoji-2601 { background-position: 0 -800px; }
-.emoji-2602 { background-position: -20px -800px; }
-.emoji-2603 { background-position: -40px -800px; }
-.emoji-2604 { background-position: -60px -800px; }
-.emoji-260E { background-position: -80px -800px; }
-.emoji-2611 { background-position: -100px -800px; }
-.emoji-2614 { background-position: -120px -800px; }
-.emoji-2615 { background-position: -140px -800px; }
-.emoji-2618 { background-position: -160px -800px; }
-.emoji-261D { background-position: -180px -800px; }
-.emoji-261D-1F3FB { background-position: -200px -800px; }
-.emoji-261D-1F3FC { background-position: -220px -800px; }
-.emoji-261D-1F3FD { background-position: -240px -800px; }
-.emoji-261D-1F3FE { background-position: -260px -800px; }
-.emoji-261D-1F3FF { background-position: -280px -800px; }
-.emoji-2620 { background-position: -300px -800px; }
-.emoji-2622 { background-position: -320px -800px; }
-.emoji-2623 { background-position: -340px -800px; }
-.emoji-2626 { background-position: -360px -800px; }
-.emoji-262A { background-position: -380px -800px; }
-.emoji-262E { background-position: -400px -800px; }
-.emoji-262F { background-position: -420px -800px; }
-.emoji-2638 { background-position: -440px -800px; }
-.emoji-2639 { background-position: -460px -800px; }
-.emoji-263A { background-position: -480px -800px; }
-.emoji-2648 { background-position: -500px -800px; }
-.emoji-2649 { background-position: -520px -800px; }
-.emoji-264A { background-position: -540px -800px; }
-.emoji-264B { background-position: -560px -800px; }
-.emoji-264C { background-position: -580px -800px; }
-.emoji-264D { background-position: -600px -800px; }
-.emoji-264E { background-position: -620px -800px; }
-.emoji-264F { background-position: -640px -800px; }
-.emoji-2650 { background-position: -660px -800px; }
-.emoji-2651 { background-position: -680px -800px; }
-.emoji-2652 { background-position: -700px -800px; }
-.emoji-2653 { background-position: -720px -800px; }
-.emoji-2660 { background-position: -740px -800px; }
-.emoji-2663 { background-position: -760px -800px; }
-.emoji-2665 { background-position: -780px -800px; }
-.emoji-2666 { background-position: -800px -800px; }
-.emoji-2668 { background-position: -820px 0; }
-.emoji-267B { background-position: -820px -20px; }
-.emoji-267F { background-position: -820px -40px; }
-.emoji-2692 { background-position: -820px -60px; }
-.emoji-2693 { background-position: -820px -80px; }
-.emoji-2694 { background-position: -820px -100px; }
-.emoji-2696 { background-position: -820px -120px; }
-.emoji-2697 { background-position: -820px -140px; }
-.emoji-2699 { background-position: -820px -160px; }
-.emoji-269B { background-position: -820px -180px; }
-.emoji-269C { background-position: -820px -200px; }
-.emoji-26A0 { background-position: -820px -220px; }
-.emoji-26A1 { background-position: -820px -240px; }
-.emoji-26AA { background-position: -820px -260px; }
-.emoji-26AB { background-position: -820px -280px; }
-.emoji-26B0 { background-position: -820px -300px; }
-.emoji-26B1 { background-position: -820px -320px; }
-.emoji-26BD { background-position: -820px -340px; }
-.emoji-26BE { background-position: -820px -360px; }
-.emoji-26C4 { background-position: -820px -380px; }
-.emoji-26C5 { background-position: -820px -400px; }
-.emoji-26C8 { background-position: -820px -420px; }
-.emoji-26CE { background-position: -820px -440px; }
-.emoji-26CF { background-position: -820px -460px; }
-.emoji-26D1 { background-position: -820px -480px; }
-.emoji-26D3 { background-position: -820px -500px; }
-.emoji-26D4 { background-position: -820px -520px; }
-.emoji-26E9 { background-position: -820px -540px; }
-.emoji-26EA { background-position: -820px -560px; }
-.emoji-26F0 { background-position: -820px -580px; }
-.emoji-26F1 { background-position: -820px -600px; }
-.emoji-26F2 { background-position: -820px -620px; }
-.emoji-26F3 { background-position: -820px -640px; }
-.emoji-26F4 { background-position: -820px -660px; }
-.emoji-26F5 { background-position: -820px -680px; }
-.emoji-26F7 { background-position: -820px -700px; }
-.emoji-26F8 { background-position: -820px -720px; }
-.emoji-26F9 { background-position: -820px -740px; }
-.emoji-26F9-1F3FB { background-position: -820px -760px; }
-.emoji-26F9-1F3FC { background-position: -820px -780px; }
-.emoji-26F9-1F3FD { background-position: -820px -800px; }
-.emoji-26F9-1F3FE { background-position: 0 -820px; }
-.emoji-26F9-1F3FF { background-position: -20px -820px; }
-.emoji-26FA { background-position: -40px -820px; }
-.emoji-26FD { background-position: -60px -820px; }
-.emoji-2702 { background-position: -80px -820px; }
-.emoji-2705 { background-position: -100px -820px; }
-.emoji-2708 { background-position: -120px -820px; }
-.emoji-2709 { background-position: -140px -820px; }
-.emoji-270A { background-position: -160px -820px; }
-.emoji-270A-1F3FB { background-position: -180px -820px; }
-.emoji-270A-1F3FC { background-position: -200px -820px; }
-.emoji-270A-1F3FD { background-position: -220px -820px; }
-.emoji-270A-1F3FE { background-position: -240px -820px; }
-.emoji-270A-1F3FF { background-position: -260px -820px; }
-.emoji-270B { background-position: -280px -820px; }
-.emoji-270B-1F3FB { background-position: -300px -820px; }
-.emoji-270B-1F3FC { background-position: -320px -820px; }
-.emoji-270B-1F3FD { background-position: -340px -820px; }
-.emoji-270B-1F3FE { background-position: -360px -820px; }
-.emoji-270B-1F3FF { background-position: -380px -820px; }
-.emoji-270C { background-position: -400px -820px; }
-.emoji-270C-1F3FB { background-position: -420px -820px; }
-.emoji-270C-1F3FC { background-position: -440px -820px; }
-.emoji-270C-1F3FD { background-position: -460px -820px; }
-.emoji-270C-1F3FE { background-position: -480px -820px; }
-.emoji-270C-1F3FF { background-position: -500px -820px; }
-.emoji-270D { background-position: -520px -820px; }
-.emoji-270D-1F3FB { background-position: -540px -820px; }
-.emoji-270D-1F3FC { background-position: -560px -820px; }
-.emoji-270D-1F3FD { background-position: -580px -820px; }
-.emoji-270D-1F3FE { background-position: -600px -820px; }
-.emoji-270D-1F3FF { background-position: -620px -820px; }
-.emoji-270F { background-position: -640px -820px; }
-.emoji-2712 { background-position: -660px -820px; }
-.emoji-2714 { background-position: -680px -820px; }
-.emoji-2716 { background-position: -700px -820px; }
-.emoji-271D { background-position: -720px -820px; }
-.emoji-2721 { background-position: -740px -820px; }
-.emoji-2728 { background-position: -760px -820px; }
-.emoji-2733 { background-position: -780px -820px; }
-.emoji-2734 { background-position: -800px -820px; }
-.emoji-2744 { background-position: -820px -820px; }
-.emoji-2747 { background-position: -840px 0; }
-.emoji-274C { background-position: -840px -20px; }
-.emoji-274E { background-position: -840px -40px; }
-.emoji-2753 { background-position: -840px -60px; }
-.emoji-2754 { background-position: -840px -80px; }
-.emoji-2755 { background-position: -840px -100px; }
-.emoji-2757 { background-position: -840px -120px; }
-.emoji-2763 { background-position: -840px -140px; }
-.emoji-2764 { background-position: -840px -160px; }
-.emoji-2795 { background-position: -840px -180px; }
-.emoji-2796 { background-position: -840px -200px; }
-.emoji-2797 { background-position: -840px -220px; }
-.emoji-27A1 { background-position: -840px -240px; }
-.emoji-27B0 { background-position: -840px -260px; }
-.emoji-27BF { background-position: -840px -280px; }
-.emoji-2934 { background-position: -840px -300px; }
-.emoji-2935 { background-position: -840px -320px; }
-.emoji-2B05 { background-position: -840px -340px; }
-.emoji-2B06 { background-position: -840px -360px; }
-.emoji-2B07 { background-position: -840px -380px; }
-.emoji-2B1B { background-position: -840px -400px; }
-.emoji-2B1C { background-position: -840px -420px; }
-.emoji-2B50 { background-position: -840px -440px; }
-.emoji-2B55 { background-position: -840px -460px; }
-.emoji-3030 { background-position: -840px -480px; }
-.emoji-303D { background-position: -840px -500px; }
-.emoji-3297 { background-position: -840px -520px; }
-.emoji-3299 { background-position: -840px -540px; }
-
-.emoji-icon {
- background-image: image-url('emoji.png');
- background-repeat: no-repeat;
- height: 20px;
- width: 20px;
-
- @media only screen and (-webkit-min-device-pixel-ratio: 2),
- only screen and (min--moz-device-pixel-ratio: 2),
- only screen and (-o-min-device-pixel-ratio: 2/1),
- only screen and (min-device-pixel-ratio: 2),
- only screen and (min-resolution: 192dpi),
- only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
- background-size: 860px 840px;
- }
+gl-emoji {
+ display: inline-block;
+ display: inline-flex;
+ vertical-align: middle;
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ font-size: 1.5em;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 30f242a35db..ffece53a093 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -271,6 +271,7 @@ span.idiff {
font-size: 13px;
line-height: 28px;
display: inline-block;
+ float: none;
}
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index e3da467a27c..51805c5d734 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,10 +1,24 @@
.filter-item {
- margin-right: 6px;
vertical-align: top;
&.reset-filters {
padding: 7px;
}
+
+ &.update-issues-btn {
+ float: right;
+ margin-right: 0;
+
+ @media (max-width: $screen-xs-max) {
+ float: none;
+ }
+ }
+}
+
+.filters-section {
+ @media (max-width: $screen-xs-max) {
+ display: inline-block;
+ }
}
@media (min-width: $screen-sm-min) {
@@ -14,6 +28,20 @@
width: 132px;
}
}
+
+ .filter-item:not(:last-child) {
+ margin-right: 6px;
+ }
+
+ .sort-filter {
+ display: inline-block;
+ float: right;
+ }
+
+ .dropdown-menu-sort {
+ left: auto;
+ right: 0;
+ }
}
@media (max-width: $screen-xs-max) {
@@ -21,11 +49,106 @@
display: block;
margin: 0 0 10px;
}
+
+ .dropdown-menu-toggle,
+ .update-issues-btn .btn {
+ width: 100%;
+ }
}
.filtered-search-container {
display: -webkit-flex;
display: flex;
+
+ @media (max-width: $screen-xs-min) {
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+
+ .tokens-container {
+ display: -webkit-flex;
+ display: flex;
+ flex: 1;
+ -webkit-flex: 1;
+ padding-left: 30px;
+ position: relative;
+ margin-bottom: 0;
+ }
+
+ .input-token {
+ max-width: 200px;
+ }
+
+ .input-token:only-child,
+ .input-token:last-child {
+ flex: 1;
+ -webkit-flex: 1;
+ max-width: initial;
+ }
+}
+
+.filtered-search-token,
+.filtered-search-term {
+ display: -webkit-flex;
+ display: flex;
+ margin-top: 5px;
+ margin-bottom: 5px;
+
+ .selectable {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .name,
+ .value {
+ display: inline-block;
+ padding: 2px 7px;
+ }
+
+ .name {
+ background-color: $filter-name-resting-color;
+ color: $filter-name-text-color;
+ border-radius: 2px 0 0 2px;
+ margin-right: 1px;
+ text-transform: capitalize;
+ }
+
+ .value {
+ background-color: $white-normal;
+ color: $filter-value-text-color;
+ border-radius: 0 2px 2px 0;
+ margin-right: 5px;
+ }
+
+ .selected {
+ .name {
+ background-color: $filter-name-selected-color;
+ }
+
+ .value {
+ background-color: $filter-value-selected-color;
+ }
+ }
+}
+
+.filtered-search-term {
+ .name {
+ background-color: inherit;
+ color: $black;
+ text-transform: none;
+ }
+
+ .selectable {
+ cursor: text;
+ }
+}
+
+.scroll-container {
+ display: -webkit-flex;
+ display: flex;
+ overflow-x: auto;
+ white-space: nowrap;
+ width: 100%;
}
.filtered-search-input-container {
@@ -33,13 +156,48 @@
display: flex;
position: relative;
width: 100%;
+ border: 1px solid $border-color;
+ background-color: $white-light;
+
+ @media (max-width: $screen-xs-min) {
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
+ margin-bottom: 10px;
+
+ .dropdown-menu {
+ width: auto;
+ left: 0;
+ right: 0;
+ max-width: none;
+ min-width: 100%;
+ }
+ }
+
+ &:hover {
+ @extend .form-control:hover;
+ }
+
+ &.focus,
+ &.focus:hover {
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ &.focus .fa-filter {
+ color: $common-gray-dark;
+ }
.form-control {
- padding-left: 25px;
- padding-right: 25px;
+ position: relative;
+ min-width: 200px;
+ padding: 5px 25px 6px 0;
+ border-color: transparent;
- &:focus ~ .fa-filter {
- color: $common-gray-dark;
+ &:focus,
+ &:hover {
+ outline: none;
+ border-color: transparent;
+ box-shadow: none;
}
}
@@ -57,12 +215,13 @@
.clear-search {
width: 35px;
- background-color: transparent;
+ background-color: $white-light;
border: none;
position: absolute;
right: 0;
height: 100%;
outline: none;
+ z-index: 1;
&:hover .fa-times {
color: $common-gray-dark;
@@ -70,6 +229,15 @@
}
}
+.filter-dropdown-container {
+ display: -webkit-flex;
+ display: flex;
+
+ .dropdown-toggle {
+ line-height: 22px;
+ }
+}
+
.dropdown-menu .filter-dropdown-item {
padding: 0;
}
@@ -79,6 +247,41 @@
overflow: auto;
}
+@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ .issues-details-filters {
+ .dropdown-menu-toggle {
+ width: 100px;
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .issues-details-filters {
+ padding: 0 0 10px;
+ background-color: $white-light;
+ border-top: 0;
+ }
+}
+
+@media (max-width: $screen-xs) {
+ .filter-dropdown-container {
+ .dropdown-toggle,
+ .dropdown {
+ width: 100%;
+ }
+
+ .dropdown {
+ margin-left: 0;
+ }
+
+ .fa-chevron-down {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+ }
+}
+
%filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color;
color: $white-light;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 78434b99b62..6660a022260 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -149,14 +149,14 @@ header {
.header-logo {
display: inline-block;
- margin: 0 8px 0 3px;
+ margin: 0 7px 0 2px;
position: relative;
- top: 7px;
+ top: 10px;
transition-duration: .3s;
svg,
img {
- height: 36px;
+ height: 28px;
}
&:hover {
@@ -164,12 +164,25 @@ header {
}
}
+ .group-name-toggle {
+ margin: 0 5px;
+ vertical-align: sub;
+ }
+
+ .group-title {
+ &.is-hidden {
+ .hidable:not(:last-of-type) {
+ display: none;
+ }
+ }
+ }
+
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 450px;
+ max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -179,10 +192,26 @@ header {
vertical-align: top;
white-space: nowrap;
+ &.initializing {
+ display: none;
+ }
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ max-width: 300px;
+ }
+
@media (max-width: $screen-xs-max) {
max-width: 190px;
}
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ max-width: 428px;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ max-width: 685px;
+ }
+
a {
color: $gl-text-color;
@@ -253,24 +282,34 @@ header {
font-size: 18px;
.navbar-nav {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
margin: 0;
- float: none !important;
-
- .visible-xs,
- .visible-sm {
- display: table-cell !important;
- }
+ text-align: right;
}
.navbar-collapse {
padding-left: 5px;
- .nav > li {
- display: table-cell;
- width: 1%;
+ .nav > li:not(.hidden-xs) {
+ display: table-cell!important;
+ width: 25%;
+
+ a {
+ margin-right: 8px;
+ }
}
}
}
+
+ .header-user-dropdown-toggle {
+ text-align: center;
+ }
+
+ .header-user-avatar {
+ float: none;
+ }
}
.header-user {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 909a0f4afda..6d27d7568cf 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -57,8 +57,13 @@
visibility: hidden;
}
- &:hover i {
- visibility: visible;
+ &:hover,
+ &:focus {
+ outline: none;
+
+ & i {
+ visibility: visible;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index d335fedefe2..300ba4f2de6 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,17 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
- &.ui-autocomplete {
- border-color: $jq-ui-border;
- padding: 0;
- margin-top: 2px;
- z-index: 1001;
-
- .ui-menu-item a {
- padding: 4px 10px;
- }
- }
-
.ui-state-default {
border: 1px solid $white-light;
background: $white-light;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 29d55c44699..0a42b17c1f5 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -8,6 +8,19 @@ body {
&.navless {
background-color: $white-light !important;
}
+
+ &.card-content {
+ background-color: $gray-darker;
+
+ .content-wrapper {
+ padding: 0;
+
+ .container-fluid,
+ .container-limited {
+ background-color: $gray-darker;
+ }
+ }
+ }
}
.container {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2bfdb9f9601..7adbb0a4188 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -96,16 +96,6 @@ ul.unstyled-list > li {
border-bottom: none;
}
-ul.task-list {
- li.task-list-item {
- list-style-type: none;
- }
-
- ul:not(.task-list) {
- padding-left: 1.3em;
- }
-}
-
// Generic content list
ul.content-list {
@include basic-list;
@@ -239,44 +229,6 @@ ul.content-list {
}
}
-// Table list
-.table-list {
- display: table;
- width: 100%;
-
- .table-list-row {
- display: table-row;
- }
-
- .table-list-cell {
- display: table-cell;
- vertical-align: top;
- padding: 10px 16px;
- border-bottom: 1px solid $gray-darker;
-
- &.avatar-cell {
- width: 36px;
- padding-right: 0;
-
- img {
- margin-right: 0;
- }
- }
- }
-
- &.table-wide {
- .table-list-cell {
- &:last-of-type {
- padding-right: 0;
- }
-
- &:first-of-type {
- padding-left: 0;
- }
- }
- }
-}
-
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index d4758d90352..a668a6c4c39 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -147,6 +147,9 @@
}
.atwho-view {
+ overflow-y: auto;
+ overflow-x: hidden;
+
small.description {
float: right;
padding: 3px 5px;
@@ -162,4 +165,8 @@
@include disableAllAnimation;
}
}
+
+ ul > li {
+ white-space: nowrap;
+ }
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1acd06122a3..df78bbdea51 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -76,6 +76,13 @@
#{$property}: $value;
}
+/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */
+@mixin on-webkit-only {
+ @media screen and (-webkit-min-device-pixel-ratio:0) {
+ @content;
+ }
+}
+
@mixin keyframes($animation-name) {
@-webkit-keyframes #{$animation-name} {
@content;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8e2c56a8488..eb73f7cc794 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -100,8 +100,7 @@
@media (max-width: $screen-sm-max) {
.issues-filters {
- .milestone-filter,
- .labels-filter {
+ .milestone-filter {
display: none;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7d4a814a36c..205d23b1329 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,7 +1,7 @@
@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
- z-index: 1;
+ z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
@@ -18,7 +18,7 @@
.fa {
position: relative;
- top: 6px;
+ top: 5px;
font-size: 18px;
}
}
@@ -79,6 +79,7 @@
}
&.sub-nav {
+ text-align: center;
background-color: $gray-normal;
.container-fluid {
@@ -137,7 +138,6 @@
.nav-links {
display: inline-block;
- width: 50%;
margin-bottom: 0;
border-bottom: none;
@@ -286,13 +286,14 @@
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
+ text-align: center;
.container-fluid {
position: relative;
.nav-control {
@media (max-width: $screen-sm-max) {
- margin-right: 75px;
+ margin-right: 2px;
}
}
}
@@ -351,7 +352,7 @@
right: -5px;
.fa {
- right: -28px;
+ right: -7px;
}
}
@@ -381,7 +382,7 @@
left: 0;
.fa {
- left: -4px;
+ left: 10px;
}
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index efe93724013..9d8d08dff88 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -48,11 +48,3 @@
line-height: inherit;
}
}
-
-.panel-default {
- .table-list-row:last-child {
- .table-list-cell {
- border-bottom: 0;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d09b1c9d7f5..40e93032f59 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -55,7 +55,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar):not(.wiki-sidebar) {
+ .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
@@ -73,10 +73,6 @@
right: $gutter_collapsed_width;
}
}
-
- &.with-overlay {
- padding-right: $gutter_collapsed_width;
- }
}
.right-sidebar {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index ea2d26dd5a0..12a86a64645 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -86,6 +86,16 @@
position: fixed;
}
+/*
+ * Fix <summary> elements on firefox
+ * See https://github.com/necolas/normalize.css/issues/640
+ * and https://github.com/twbs/bootstrap/issues/21060
+ *
+ */
+summary {
+ display: list-item;
+}
+
@import "bootstrap/responsive-utilities";
// Labels
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 54958973f15..c241816788b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -134,7 +134,7 @@
ul,
ol {
padding: 0;
- margin: 3px 0 3px 28px !important;
+ margin: 3px 0 !important;
}
ul:dir(rtl),
@@ -144,6 +144,29 @@
li {
line-height: 1.6em;
+ margin-left: 25px;
+ padding-left: 3px;
+
+ /* Normalize the bullet position on webkit. */
+ @include on-webkit-only {
+ margin-left: 28px;
+ padding-left: 0;
+ }
+ }
+
+ ul.task-list {
+ li.task-list-item {
+ list-style-type: none;
+ position: relative;
+ padding-left: 28px;
+ margin-left: 0 !important;
+
+ input.task-list-item-checkbox {
+ position: absolute;
+ left: 8px;
+ top: 5px;
+ }
+ }
}
a[href*="/uploads/"],
@@ -283,6 +306,11 @@ a > code {
* Textareas intended for GFM
*
*/
+textarea.js-gfm-input {
+ font-family: $monospace_font;
+ font-size: 13px;
+}
+
.strikethrough {
text-decoration: line-through;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ba0af072716..6841adb637e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1;
* Fonts
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
/*
* Dropdowns
@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
+
+/*
+Filtered Search
+*/
+$filter-name-resting-color: #f8f8f8;
+$filter-name-text-color: rgba(0, 0, 0, 0.55);
+$filter-value-text-color: rgba(0, 0, 0, 0.85);
+$filter-name-selected-color: #ebebeb;
+$filter-value-selected-color: #d7d7d7;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 97ade638db6..0c226ff7598 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -20,8 +20,9 @@
outline: none;
resize: none;
height: 100vh;
+ max-height: calc(100vh - 10px);
max-width: 900px;
- margin: 0 auto;
+ margin: 0 auto 10px;
}
.zen-control-leave {
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6f2e746d4b0..09951fe3d3e 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -20,6 +20,8 @@ $dark-highlight-bg: #ffe792;
$dark-highlight-color: $black;
$dark-pre-hll-bg: #373b41;
$dark-hll-bg: #373b41;
+$dark-over-bg: #9f9ab5;
+$dark-expanded-bg: #3e3e3e;
$dark-c: #969896;
$dark-err: #c66;
$dark-k: #b294bb;
@@ -139,9 +141,37 @@ $dark-il: #de935f;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $dark-over-bg;
+ border-color: darken($dark-over-bg, 5%);
+
+ a {
+ color: darken($dark-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $black;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $dark-expanded-bg;
+ border-color: $dark-expanded-bg;
+ }
+ }
}
// highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 2144a5f7466..b6a6d298adf 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -13,6 +13,8 @@ $monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080;
$monokai-highlight-bg: #ffe792;
+$monokai-over-bg: #9f9ab5;
+$monokai-expanded-bg: #3e3e3e;
$monokai-new-bg: rgba(166, 226, 46, 0.1);
$monokai-new-idiff: rgba(166, 226, 46, 0.15);
@@ -139,9 +141,37 @@ $monokai-gi: #a6e22e;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $monokai-over-bg;
+ border-color: darken($monokai-over-bg, 5%);
+
+ a {
+ color: darken($monokai-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $black;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $monokai-expanded-bg;
+ border-color: $monokai-expanded-bg;
+ }
+ }
}
// highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 2cb1d18f12f..4f7a50dcb4f 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -17,6 +17,8 @@ $solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652;
+$solarized-dark-over-bg: #9f9ab5;
+$solarized-dark-expanded-bg: #010d10;
$solarized-dark-c: #586e75;
$solarized-dark-err: #93a1a1;
$solarized-dark-g: #93a1a1;
@@ -143,9 +145,37 @@ $solarized-dark-il: #2aa198;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $solarized-dark-over-bg;
+ border-color: darken($solarized-dark-over-bg, 5%);
+
+ a {
+ color: darken($solarized-dark-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $black;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $solarized-dark-expanded-bg;
+ border-color: $solarized-dark-expanded-bg;
+ }
+ }
}
// highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index b72c4326730..6463fe96c1b 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -18,6 +18,9 @@ $solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5;
+$solarized-light-over-bg: #ded7fc;
+$solarized-light-expanded-border: #d2cdbd;
+$solarized-light-expanded-bg: #ece6d4;
$solarized-light-c: #93a1a1;
$solarized-light-err: #586e75;
$solarized-light-g: #586e75;
@@ -150,9 +153,37 @@ $solarized-light-il: #2aa198;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $solarized-light-over-bg;
+ border-color: darken($solarized-light-over-bg, 5%);
+
+ a {
+ color: darken($solarized-light-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include matchLine;
}
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $solarized-light-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $solarized-light-expanded-bg;
+ border-color: $solarized-light-expanded-bg;
+ }
+ }
}
// highlight line via anchor
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 398fbfd3b18..ab2018bfbca 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -7,6 +7,9 @@ $white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
+$white-over-bg: #ded7fc;
+$white-expanded-border: #e0e0e0;
+$white-expanded-bg: #f7f7f7;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
@@ -123,12 +126,38 @@ $white-gc-bg: #eaf2f5;
}
}
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $white-over-bg;
+ border-color: darken($white-over-bg, 5%);
+
+ a {
+ color: darken($white-over-bg, 15%);
+ }
+ }
+
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $white-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $white-expanded-bg;
+ border-color: $white-expanded-bg;
+ }
+ }
+
.line_content {
&.old {
background-color: $line-removed;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9a36d76136b..f9ee33019cd 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -420,12 +420,9 @@
display: -webkit-flex;
display: flex;
- .form-control {
- margin-left: auto;
-
- @media (min-width: $screen-sm-min) {
- max-width: 200px;
- }
+ .issues-filters {
+ -webkit-flex: 1;
+ flex: 1;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c3d45d708c1..da8410eca66 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -38,6 +38,38 @@
}
}
+.pipeline-info {
+ .status-icon-container {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 3px;
+
+ svg {
+ display: block;
+ width: 22px;
+ height: 22px;
+ }
+ }
+
+ .mr-widget-pipeline-graph {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 0 -6px 0 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+ }
+}
+
+.branch-info .commit-icon {
+ margin-right: 3px;
+
+ svg {
+ top: 3px;
+ }
+}
+
/*
* Commit message textarea for web editor and
* custom merge request message
@@ -78,6 +110,7 @@
padding: 5px 10px;
background-color: $gray-light;
border-bottom: 1px solid $gray-darker;
+ border-top: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
@@ -117,10 +150,37 @@
}
}
+.commit.flex-list {
+ display: flex;
+}
+
+.avatar-cell {
+ width: 46px;
+ padding-left: 10px;
+
+ img {
+ margin-right: 0;
+ }
+}
+
+.commit-detail {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-grow: 1;
+ padding-left: 10px;
+
+ .merge-request-branches & {
+ flex-direction: column;
+ }
+}
+
+.commit-content {
+ padding-right: 10px;
+}
+
.commit-actions {
@media (min-width: $screen-sm-min) {
- width: 300px;
- text-align: right;
font-size: 0;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 5b777953fb0..ad3dbc7ac48 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -369,13 +369,11 @@
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
- position: relative;
- top: 3px;
- left: 1px;
- display: inline-block;
-
svg {
- float: left;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ vertical-align: bottom;
}
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 92d7772da57..eab79c2a481 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -89,6 +89,10 @@
.diff-line-num {
width: 50px;
+
+ a {
+ transition: none;
+ }
}
.line_holder td {
@@ -111,7 +115,7 @@
}
.add-diff-note {
- margin-left: -65px;
+ margin-left: -55px;
}
}
@@ -133,8 +137,13 @@
width: 35px;
font-weight: normal;
- &:hover {
- text-decoration: underline;
+ &[disabled] {
+ cursor: default;
+
+ &:hover,
+ &:active {
+ text-decoration: none;
+ }
}
}
}
@@ -485,3 +494,103 @@
}
}
}
+
+.diff-comment-avatar-holders {
+ position: absolute;
+ height: 19px;
+ width: 19px;
+ margin-left: -15px;
+
+ &:hover {
+ .diff-comment-avatar,
+ .diff-comments-more-count {
+ @for $i from 1 through 4 {
+ $x-pos: 14px;
+
+ &:nth-child(#{$i}) {
+ @if $i == 4 {
+ $x-pos: 14.5px;
+ }
+
+ transform: translateX((($i * $x-pos) - $x-pos));
+
+ &:hover {
+ transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
+ }
+ }
+ }
+ }
+
+ .diff-comments-more-count {
+ padding-left: 2px;
+ padding-right: 2px;
+ width: auto;
+ }
+ }
+}
+
+.diff-comment-avatar,
+.diff-comments-more-count {
+ position: absolute;
+ left: 0;
+ width: 19px;
+ height: 19px;
+ margin-right: 0;
+ border-color: $white-light;
+ cursor: pointer;
+ transition: all .1s ease-out;
+
+ @for $i from 1 through 4 {
+ &:nth-child(#{$i}) {
+ z-index: (4 - $i);
+ }
+ }
+}
+
+.diff-comments-more-count {
+ width: 19px;
+ min-width: 19px;
+ padding-left: 0;
+ padding-right: 0;
+ overflow: hidden;
+}
+
+.diff-comments-more-count,
+.diff-notes-collapse {
+ background-color: $gray-darkest;
+ color: $white-light;
+ border: 1px solid $white-light;
+ border-radius: 1em;
+ font-family: $regular_font;
+ font-size: 9px;
+ line-height: 17px;
+ text-align: center;
+}
+
+.diff-notes-collapse {
+ position: relative;
+ width: 19px;
+ height: 19px;
+ padding: 0;
+ transition: transform .1s ease-out;
+
+ svg {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-left: -5.5px;
+ margin-top: -5.5px;
+ }
+
+ path {
+ fill: $white-light;
+ }
+
+ &:hover {
+ transform: scale(1.2);
+ }
+
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 181dcb7721f..73a5da715f2 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -15,96 +15,97 @@
padding-top: 20px;
}
-@media (max-width: $screen-xs-max) {
- .environments-container {
+.environments-container {
+ .table-holder {
width: 100%;
overflow: auto;
}
-}
-
-.environments {
- table-layout: fixed;
- .environments-commit,
- .environments-actions,
- .environments-deploy,
- .environments-build,
- .environments-date {
- position: static;
- float: none;
- display: table-cell;
- }
+ .table.ci-table {
+ .environments-actions {
+ min-width: 200px;
+ }
- .environments-name,
- .environments-commit,
- .environments-actions {
- width: 20%;
- }
+ .environments-commit,
+ .environments-actions {
+ width: 20%;
+ }
- .environments-date {
- width: 10%;
- }
+ .environments-date {
+ width: 10%;
+ }
- .environments-deploy,
- .environments-build {
- width: 15%;
- }
+ .environments-name,
+ .environments-deploy,
+ .environments-build {
+ width: 15%;
+ }
- .environment-name,
- .environments-build-cell,
- .deployment-column {
- word-break: break-all;
- }
+ .deployment-column {
+ > span {
+ word-break: break-all;
+ }
- .deployment-column {
- .avatar {
- float: none;
+ .avatar {
+ float: none;
+ }
}
- }
- .commit-title {
- margin: 0;
- }
+ .btn-group {
- .avatar-image-container {
- text-decoration: none;
- }
+ > a {
+ color: $gl-text-color-secondary;
+ }
- .icon-play {
- height: 13px;
- width: 12px;
- }
+ svg path {
+ fill: $gl-text-color-secondary;
+ }
- .external-url,
- .dropdown-new {
- color: $gl-text-color-secondary;
- }
+ .dropdown {
+ outline: none;
+ }
+ }
- .dropdown-menu {
+ .commit-title {
+ margin: 0;
+ }
- .fa {
- margin-right: 6px;
- color: $gl-text-color-secondary;
+ .avatar-image-container {
+ text-decoration: none;
}
- }
- .build-link,
- .branch-name {
- color: $gl-text-color;
- }
+ .icon-play {
+ height: 13px;
+ width: 12px;
+ }
- .stop-env-link,
- .external-url {
- color: $gl-text-color-secondary;
+ .external-url,
+ .dropdown-new {
+ color: $gl-text-color-secondary;
+ }
- .stop-env-icon {
- font-size: 14px;
+ .dropdown-menu {
+ .fa {
+ margin-right: 6px;
+ color: $gl-text-color-secondary;
+ }
}
- }
- .deployment {
- .build-column {
+ .build-link,
+ .branch-name {
+ color: $gl-text-color;
+ }
+
+ .stop-env-link,
+ .external-url {
+ color: $gl-text-color-secondary;
+
+ .stop-env-icon {
+ font-size: 14px;
+ }
+ }
+ .deployment .build-column {
.build-link {
color: $gl-text-color;
}
@@ -113,34 +114,108 @@
float: none;
}
}
- }
-
- .folder-icon {
- margin-right: 3px;
- color: $gl-text-color-secondary;
- display: inline-block;
- .fa:nth-child(1) {
+ .folder-icon {
margin-right: 3px;
+ color: $gl-text-color-secondary;
+ display: inline-block;
+
+ .fa:nth-child(1) {
+ margin-right: 3px;
+ }
+ }
+
+ .folder-name {
+ cursor: pointer;
+ color: $gl-text-color-secondary;
+ display: inline-block;
}
- }
- .folder-name {
- cursor: pointer;
- color: $gl-text-color-secondary;
- display: inline-block;
+ .icon-container {
+ width: 20px;
+ text-align: center;
+ }
+
+ .branch-commit {
+ .commit-id {
+ margin-right: 0;
+ }
+ }
+
+ .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+ }
}
}
-.table.ci-table.environments {
- .icon-container {
- width: 20px;
- text-align: center;
+.prometheus-graph {
+ text {
+ fill: $stat-graph-axis-fill;
}
+}
- .branch-commit {
- .commit-id {
- margin-right: 0;
- }
+.x-axis path,
+.y-axis path,
+.label-x-axis-line,
+.label-y-axis-line {
+ fill: none;
+ stroke-width: 1;
+ shape-rendering: crispEdges;
+}
+
+.x-axis path,
+.y-axis path {
+ stroke: $stat-graph-axis-fill;
+}
+
+.label-x-axis-line,
+.label-y-axis-line {
+ stroke: $border-color;
+}
+
+.y-axis {
+ line {
+ stroke: $stat-graph-axis-fill;
+ stroke-width: 1;
}
}
+
+.metric-area {
+ opacity: 0.8;
+}
+
+.prometheus-graph-overlay {
+ fill: none;
+ opacity: 0.0;
+ pointer-events: all;
+}
+
+.rect-text-metric {
+ fill: $white-light;
+ stroke-width: 1;
+ stroke: $black;
+}
+
+.rect-axis-text {
+ fill: $white-light;
+}
+
+.text-metric,
+.text-median-metric,
+.text-metric-usage,
+.text-metric-date {
+ fill: $black;
+}
+
+.text-metric-date {
+ font-weight: 200;
+}
+
+.selected-metric-line {
+ stroke: $black;
+ stroke-width: 1;
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 5776d86983a..08398bb43a2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -155,7 +155,7 @@
@media (max-width: $screen-xs-max) {
.event-item {
- padding-left: $gl-padding;
+ padding-left: 0;
.event-title {
white-space: normal;
@@ -169,8 +169,7 @@
.event-body {
margin: 0;
- border-left: 2px solid $events-body-border;
- padding-left: 10px;
+ padding-left: 0;
}
.event-item-timestamp {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index d377526e655..84d21e48463 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -73,3 +73,19 @@
}
}
}
+
+.mattermost-icon svg {
+ width: 16px;
+ height: 16px;
+ vertical-align: text-bottom;
+}
+
+.mattermost-team-name {
+ color: $gl-text-color-secondary;
+}
+
+.mattermost-info {
+ display: block;
+ color: $gl-text-color-secondary;
+ margin-top: 10px;
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 80b0c9493d8..cb7ebd61504 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -10,6 +10,11 @@
.issue-labels {
display: inline-block;
}
+
+ .icon-merge-request-unmerged {
+ height: 13px;
+ margin-bottom: 3px;
+ }
}
}
@@ -51,7 +56,10 @@ ul.related-merge-requests > li {
.merge-request-id {
display: inline-block;
- width: 3em;
+}
+
+.merge-request-info {
+ margin-left: 5px;
}
.merge-request-status {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0b0c4bc130d..7c3172421c1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,7 +3,6 @@
*
*/
.mr-state-widget {
- background: $gray-light;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
@@ -25,11 +24,7 @@
color: inherit;
}
- .btn-success.dropdown-toggle:disabled {
- background-color: $gl-success;
- }
-
- .accept_merge_request {
+ .accept-merge-request {
&.ci-pending,
&.ci-running {
@include btn-blue;
@@ -42,6 +37,12 @@
@include btn-red;
}
}
+
+ .dropdown-toggle {
+ .fa {
+ color: inherit;
+ }
+ }
}
.accept-control {
@@ -103,12 +104,17 @@
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
+
+ .ci-status-icon > .icon-link > svg {
+ width: 22px;
+ height: 22px;
+ }
}
.mr-widget-body,
.ci_widget,
.mr-widget-footer {
- padding: $gl-padding;
+ padding: 16px;
}
.mr-widget-pipeline-graph {
@@ -168,10 +174,6 @@
}
}
- p:last-child {
- margin-bottom: 0;
- }
-
.btn-grouped {
margin-left: 0;
margin-right: 7px;
@@ -234,8 +236,7 @@
.commit {
margin: 0;
- padding-top: 2px;
- padding-bottom: 2px;
+ padding: 10px 0;
list-style: none;
&:hover {
@@ -334,8 +335,61 @@
}
}
+.remove-message-pipes {
+ ul {
+ margin: 10px 0 0 12px;
+ padding: 0;
+ list-style: none;
+ border-left: 2px solid $border-color;
+ display: inline-block;
+ }
+
+ li {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ display: block;
+
+ span {
+ margin-left: 15px;
+ max-height: 20px;
+ }
+ }
+
+ li::before {
+ content: '';
+ position: absolute;
+ border-top: 2px solid $border-color;
+ height: 1px;
+ top: 8px;
+ width: 8px;
+ }
+
+ li:last-child {
+ &::before {
+ top: 18px;
+ }
+
+ span {
+ display: block;
+ position: relative;
+ top: 5px;
+ margin-top: 5px;
+ }
+ }
+}
+
.mr-source-target {
+ background-color: $gray-light;
line-height: 31px;
+ border-style: solid;
+ border-width: 1px;
+ border-color: $border-color;
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px;
+ border-bottom: none;
+ padding: 16px;
+ margin-bottom: -1px;
}
.panel-new-merge-request {
@@ -350,7 +404,7 @@
}
.panel-footer {
- padding: 5px 10px;
+ padding: 0;
.btn {
min-width: auto;
@@ -420,6 +474,11 @@
}
}
+.assign-to-me-link {
+ padding-left: 12px;
+ white-space: nowrap;
+}
+
.table-holder {
.ci-table {
@@ -431,6 +490,8 @@
}
.merged-buttons {
+ margin-top: 20px;
+
.btn {
float: left;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f984b469609..927bf9805ce 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
+
+ &.alert-dismissable {
+ .close {
+ color: $white-light;
+ opacity: 0.85;
+ font-weight: normal;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
}
.discussion-body,
@@ -178,8 +190,25 @@
padding-right: 5px;
}
- &:last-child {
- padding-left: 5px;
+ }
+
+ .discussion-actions {
+ display: table;
+
+ .new-issue-for-discussion path {
+ fill: $gray-darkest;
+ }
+
+ .btn-group {
+ display: table-cell;
+
+ &:first-child {
+ padding-right: 0;
+ }
+
+ &:first-child:not(:last-child) > div {
+ border-right: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index aa130a1abb0..e238f0865f6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -331,6 +331,10 @@ ul.notes {
&:hover {
color: $gl-link-color;
+ }
+
+ &:focus,
+ &:hover {
text-decoration: none;
}
}
@@ -380,7 +384,7 @@ ul.notes {
top: 0;
.note-action-button {
- margin-left: 10px;
+ margin-left: 8px;
}
}
@@ -396,8 +400,7 @@ ul.notes {
}
.note-action-button {
- display: inline-block;
- margin-left: 0;
+ display: inline;
line-height: 20px;
@media (min-width: $screen-sm-min) {
@@ -452,36 +455,37 @@ ul.notes {
* Line note button on the side of diffs
*/
-.diff-file tr.line_holder {
- @mixin show-add-diff-note {
- display: inline-block;
- }
+.add-diff-note {
+ display: none;
+ margin-top: -2px;
+ border-radius: 50%;
+ background: $white-light;
+ padding: 1px 5px;
+ font-size: 12px;
+ color: $gl-link-color;
+ margin-left: -55px;
+ position: absolute;
+ z-index: 10;
+ width: 23px;
+ height: 23px;
+ border: 1px solid $border-color;
+ transition: transform .1s ease-in-out;
- .add-diff-note {
- margin-top: -8px;
- border-radius: 40px;
- background: $white-light;
- padding: 4px;
- font-size: 16px;
- color: $gl-link-color;
- margin-left: -56px;
- position: absolute;
- z-index: 10;
- width: 32px;
- // "hide" it by default
- display: none;
+ &:hover {
+ background: $gl-info;
+ color: $white-light;
+ transform: scale(1.15);
+ }
- &:hover {
- background: $gl-info;
- color: $white-light;
- @include show-add-diff-note;
- }
+ &:active {
+ outline: 0;
}
+}
- // "show" the icon also if we just hover somewhere over the line
- &:hover > td {
+.diff-file {
+ .is-over {
.add-diff-note {
- @include show-add-diff-note;
+ display: inline-block;
}
}
}
@@ -505,6 +509,7 @@ ul.notes {
}
.line-resolve-all-container {
+
.btn-group {
margin-left: -4px;
}
@@ -513,6 +518,27 @@ ul.notes {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
+
+ .btn.discussion-create-issue-btn {
+ margin-left: -4px;
+ border-radius: 0;
+ border-right: 0;
+
+ a {
+ padding: 0;
+ line-height: 0;
+
+ &:hover {
+ text-decoration: none;
+ border: 0;
+ }
+ }
+
+ .new-issue-for-discussion path {
+ fill: $gray-darkest;
+ }
+ }
+
}
.line-resolve-all {
@@ -535,7 +561,6 @@ ul.notes {
}
.line-resolve-btn {
- display: inline-block;
position: relative;
top: 2px;
padding: 0;
@@ -558,8 +583,9 @@ ul.notes {
}
svg {
- position: relative;
fill: $gray-darkest;
+ height: 15px;
+ width: 15px;
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3fe1eef307e..33b38ca6923 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -13,21 +13,16 @@
white-space: nowrap;
}
- .commit-title {
- margin: 0;
- }
-
- .controls {
- white-space: nowrap;
+ .table-holder {
+ width: 100%;
+ overflow: auto;
}
- .btn {
- margin: 4px;
+ .commit-title {
+ margin: 0;
}
.table.ci-table {
- min-width: 1200px;
- table-layout: fixed;
.label {
margin-bottom: 3px;
@@ -37,16 +32,67 @@
color: $black;
}
- .pipeline-date,
- .pipeline-status {
- width: 10%;
+ .stage-cell {
+ min-width: 130px; // Guarantees we show at least 4 stages in line
+ width: 20%;
+ }
+
+ .pipelines-time-ago {
+ text-align: right;
}
- .pipeline-info,
- .pipeline-commit,
- .pipeline-stages,
.pipeline-actions {
- width: 20%;
+ padding-right: 0;
+ min-width: 170px; //Guarantees buttons don't break in several lines.
+
+ .btn-default {
+ color: $gl-text-color-secondary;
+ }
+
+ .btn.btn-retry:hover,
+ .btn.btn-retry:focus {
+ border-color: $gray-darkest;
+ background-color: $white-normal;
+ }
+
+ svg path {
+ fill: $gl-text-color-secondary;
+ }
+
+ .dropdown-menu {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+
+ .dropdown-toggle,
+ .dropdown-menu {
+ color: $gl-text-color-secondary;
+
+ .fa {
+ color: $gl-text-color-secondary;
+ font-size: 14px;
+ }
+ }
+
+ .btn-group {
+ &.open {
+ .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
+ }
+ }
+
+ .btn {
+ .icon-play {
+ height: 13px;
+ width: 12px;
+ }
+ }
+ }
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
}
}
@@ -54,6 +100,7 @@
@media (max-width: $screen-md-max) {
.content-list {
&.pipelines,
+ &.environments-container,
&.builds-content-list {
width: 100%;
overflow: auto;
@@ -61,27 +108,10 @@
}
}
-.content-list.pipelines .table-holder {
- min-height: 300px;
-}
-
-.pipeline-holder {
- width: 100%;
- overflow: auto;
-}
-
.table.ci-table {
- min-width: 900px;
- &.pipeline {
- min-width: 650px;
- }
-
- &.builds-page {
-
- tr {
- height: 71px;
- }
+ &.builds-page tbody tr {
+ height: 71px;
}
tr {
@@ -94,12 +124,16 @@
padding: 10px 8px;
}
+ td.environments-actions {
+ padding-right: 0;
+ }
+
td.stage-cell {
padding: 10px 0;
}
.commit-link {
- padding: 9px 8px 10px;
+ padding: 9px 8px 10px 2px;
}
}
@@ -206,72 +240,8 @@
}
}
- .pipeline-actions {
- min-width: 140px;
-
- .btn {
- margin: 0;
- color: $gl-text-color-secondary;
- }
-
- .cancel-retry-btns {
- vertical-align: middle;
-
- .btn:not(:first-child) {
- margin-left: 8px;
- }
- }
-
- .dropdown-menu {
- max-height: 250px;
- overflow-y: auto;
- }
-
- .dropdown-toggle,
- .dropdown-menu {
- color: $gl-text-color-secondary;
-
- .fa {
- color: $gl-text-color-secondary;
- font-size: 14px;
- }
-
- svg,
- .fa {
- margin-right: 0;
- }
- }
-
- .btn-remove {
- color: $white-light;
- }
-
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
-
- .btn {
- .icon-play {
- height: 13px;
- width: 12px;
- }
- }
- }
-
- .tooltip {
- white-space: nowrap;
- }
- }
-
- .build-link {
-
- a {
- color: $gl-text-color;
- }
+ .build-link a {
+ color: $gl-text-color;
}
.btn-group.open .dropdown-toggle {
@@ -335,31 +305,8 @@
}
.tab-pane {
- &.pipelines {
- .ci-table {
- min-width: 900px;
- }
-
- .content-list.pipelines {
- overflow: auto;
- }
-
- .stage {
- max-width: 100px;
- width: 100px;
- }
-
- .pipeline-actions {
- min-width: initial;
- }
- }
-
- &.builds {
- .ci-table {
- tr {
- height: 71px;
- }
- }
+ &.builds .ci-table tr {
+ height: 71px;
}
}
@@ -969,3 +916,22 @@
}
}
}
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8031c4467a4..1a983d8c9ef 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -277,3 +277,42 @@ table.u2f-registrations {
padding-left: 18px;
}
}
+
+.user-callout {
+ margin: 0 auto;
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .landing {
+ margin-top: $gl-padding;
+ margin-bottom: $gl-padding;
+
+ .close {
+ margin-right: 20px;
+ }
+
+ .dismiss-icon {
+ float: right;
+ cursor: pointer;
+ color: $cycle-analytics-dismiss-icon-color;
+ }
+
+ .svg-container {
+ text-align: center;
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
+ }
+ }
+
+ @media(max-width: $screen-xs-max) {
+ .inner-content {
+ padding-left: 30px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8c0de314420..efa47be9a73 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -494,11 +494,11 @@ a.deploy-project-label {
.project-stats {
font-size: 0;
text-align: center;
- border-bottom: 1px solid $border-color;
.nav {
padding-top: 12px;
padding-bottom: 12px;
+ border-bottom: 1px solid $border-color;
}
.nav > li {
@@ -638,39 +638,22 @@ pre.light-well {
margin: 0;
}
-.activity-filter-block {
- .controls {
- padding-bottom: 7px;
- margin-top: 8px;
- border-bottom: 1px solid $border-color;
- }
-}
-
.commits-search-form {
.input-short {
min-width: 200px;
}
}
-.container-fluid.project-stats-container {
- @media (max-width: $screen-xs-max) {
- padding: 12px 0;
- }
-}
-
.project-last-commit {
background-color: $gray-light;
- padding: 12px $gl-padding;
border: 1px solid $border-color;
+ border-radius: $border-radius-base;
+ padding: 12px;
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
}
- @media (min-width: $screen-sm-min) {
- border-radius: $border-radius-base;
- }
-
.ci-status {
margin-right: $gl-padding;
}
@@ -763,6 +746,8 @@ pre.light-well {
}
.protected-branches-list {
+ margin-bottom: 30px;
+
a {
color: $gl-text-color;
@@ -810,7 +795,8 @@ pre.light-well {
}
.project-refs-form .dropdown-menu,
-.dropdown-menu-projects {
+.dropdown-menu-projects,
+.dropdown-menu-branches {
width: 300px;
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 88ea92c5afb..543d2ece3df 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -182,7 +182,8 @@ input[type="checkbox"]:hover {
display: flex;
}
- .search-field-holder {
+ .search-field-holder,
+ .project-filter-form {
-webkit-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
@@ -201,7 +202,8 @@ input[type="checkbox"]:hover {
pointer-events: none;
}
- .search-text-input {
+ .search-text-input,
+ .project-filter-form-field {
padding-left: $gl-padding + 15px;
padding-right: $gl-padding + 15px;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a28a87ed4f8..3889deee21a 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,3 +24,14 @@
.service-settings .control-label {
padding-top: 0;
}
+
+.token-token-container {
+ #impersonation-token-token {
+ width: 80%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
new file mode 100644
index 00000000000..b97a29cd1a0
--- /dev/null
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -0,0 +1,12 @@
+.triggers-container {
+ .label-container {
+ display: inline-block;
+ margin-left: 10px;
+ }
+}
+
+.trigger-actions {
+ .btn {
+ margin-left: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index af9ddb9ff80..5f0aede4f5e 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -170,7 +170,11 @@
@media (max-width: $screen-sm-max) {
.todos-filters {
.dropdown-menu-toggle {
- width: 135px;
+ width: 130px;
+ }
+
+ .dropdown-menu-toggle-sort {
+ width: auto;
}
}
}
@@ -200,10 +204,6 @@
}
.todos-filters {
- .row-content-block {
- padding-bottom: 50px;
- }
-
.dropdown-menu-toggle {
width: 100%;
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e4487dbcb87..fc4da4c495f 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -139,18 +139,10 @@
.blob-commit-info {
list-style: none;
background: $gray-light;
- padding: 6px 0;
+ padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
-
- .table-list-cell {
- border-bottom: none;
- }
-
- .commit-actions {
- width: 260px;
- }
}
#modal-remove-blob > .modal-dialog { width: 850px; }
@@ -178,3 +170,29 @@
margin-left: $btn-side-margin;
}
}
+
+.repo-charts {
+ .sub-header {
+ margin: 20px 0;
+ }
+
+ .sub-header-block.border-top {
+ margin-top: 20px;
+ padding: 0;
+ border-top: 1px solid $white-dark;
+ border-bottom: none;
+ }
+
+ .commit-stats li {
+ font-size: 16px;
+ }
+
+ .tree-ref-header {
+ margin-bottom: 20px;
+
+ h4 {
+ margin: 0;
+ line-height: 36px;
+ }
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index fb6df1a06d2..1d0bd6e0b81 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -89,6 +89,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key,
:akismet_enabled,
:container_registry_token_expire_delay,
+ :default_artifacts_expire_in,
:default_branch_protection,
:default_group_visibility,
:default_project_visibility,
@@ -143,6 +144,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:two_factor_grace_period,
:user_default_external,
:user_oauth_applications,
+ :unique_ips_limit_per_user,
+ :unique_ips_limit_time_window,
+ :unique_ips_limit_enabled,
:version_check_enabled,
:terminal_max_session_time,
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 62f62e99a97..9c9f420c1e0 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
- before_action :load_scopes, only: [:new, :edit]
+ before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index 241c7be0ea1..caf4c138da8 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -1,5 +1,5 @@
class Admin::HealthCheckController < Admin::ApplicationController
def show
- @errors = HealthCheck::Utils.process_checks('standard')
+ @errors = HealthCheck::Utils.process_checks(['standard'])
end
end
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
new file mode 100644
index 00000000000..07c8bf714fc
--- /dev/null
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -0,0 +1,53 @@
+class Admin::ImpersonationTokensController < Admin::ApplicationController
+ before_action :user
+
+ def index
+ set_index_vars
+ end
+
+ def create
+ @impersonation_token = finder.build(impersonation_token_params)
+
+ if @impersonation_token.save
+ flash[:impersonation_token] = @impersonation_token.token
+ redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
+ else
+ set_index_vars
+ render :index
+ end
+ end
+
+ def revoke
+ @impersonation_token = finder.find(params[:id])
+
+ if @impersonation_token.revoke!
+ flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!"
+ else
+ flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}."
+ end
+
+ redirect_to admin_user_impersonation_tokens_path
+ end
+
+ private
+
+ def user
+ @user ||= User.find_by!(username: params[:user_id])
+ end
+
+ def finder(options = {})
+ PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
+ end
+
+ def impersonation_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
+ end
+
+ def set_index_vars
+ @scopes = Gitlab::Auth::API_SCOPES
+
+ @impersonation_token ||= finder.build
+ @inactive_impersonation_tokens = finder(state: 'inactive').execute
+ @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
+ end
+end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 39c8c6d8a0c..daecfc832bf 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects })
+ }
+ end
+ end
end
def show
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index 1330399a836..99039724521 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -3,7 +3,7 @@ class Admin::SystemInfoController < Admin::ApplicationController
'nobrowse',
'read-only',
'ro'
- ]
+ ].freeze
EXCLUDED_MOUNT_TYPES = [
'autofs',
@@ -27,7 +27,7 @@ class Admin::SystemInfoController < Admin::ApplicationController
'tmpfs',
'tracefs',
'vfat'
- ]
+ ].freeze
def show
@cpus = Vmstat.cpu rescue nil
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 7ffde71c3b1..24504685e48 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
- if user.blocked?
- flash[:alert] = "You cannot impersonate a blocked user"
-
- redirect_to admin_user_path(user)
- else
+ if can?(user, :log_in)
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
@@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController
flash[:alert] = "You are now impersonating #{user.username}"
redirect_to root_path
+ else
+ flash[:alert] =
+ if user.blocked?
+ "You cannot impersonate a blocked user"
+ elsif user.internal?
+ "You cannot impersonate an internal user"
+ else
+ "You cannot impersonate a user who cannot log in"
+ end
+
+ redirect_to admin_user_path(user)
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 5e7af3bff0d..b7ce081a5cd 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
render_403
end
+ rescue_from Gitlab::Auth::TooManyIps do |e|
+ head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
@@ -63,7 +67,7 @@ class ApplicationController < ActionController::Base
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
- if user
+ if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
@@ -72,14 +76,6 @@ class ApplicationController < ActionController::Base
end
end
- def authenticate_user!(*args)
- if redirect_to_home_page_url?
- return redirect_to current_application_settings.home_page_url
- end
-
- super(*args)
- end
-
def log_exception(exception)
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
application_trace.map!{ |t| " #{t}\n" }
@@ -94,7 +90,7 @@ class ApplicationController < ActionController::Base
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -130,10 +126,6 @@ class ApplicationController < ActionController::Base
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-UA-Compatible'] = 'IE=edge'
headers['X-Content-Type-Options'] = 'nosniff'
- # Enabling HSTS for non-standard ports would send clients to the wrong port
- if Gitlab.config.gitlab.https && Gitlab.config.gitlab.port == 443
- headers['Strict-Transport-Security'] = 'max-age=31536000'
- end
end
def validate_user_service_ticket!
@@ -181,7 +173,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_ldap_access(&block)
- Gitlab::LDAP::Access.open { |access| block.call(access) }
+ Gitlab::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object
@@ -287,19 +279,6 @@ class ApplicationController < ActionController::Base
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
- return false unless current_application_settings.home_page_url.present?
-
- home_page_url = current_application_settings.home_page_url.chomp('/')
- root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
-
- return false if root_urls.include?(home_page_url)
-
- current_user.nil? && root_path == request.path
- end
-
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d7a45bacd35..b79ca034c5b 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
- @users = @users.where.not(id: current_user.id)
- @users = [current_user, *@users]
+ @users = [current_user, *@users].uniq
end
if params[:author_id].present?
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
deleted file mode 100644
index ff297d6ff13..00000000000
--- a/app/controllers/ci/projects_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Ci
- class ProjectsController < ::ApplicationController
- before_action :project
- before_action :no_cache, only: [:badge]
- before_action :authorize_read_project!, except: [:badge, :index]
- skip_before_action :authenticate_user!, only: [:badge]
- protect_from_forgery
-
- def index
- redirect_to root_path
- end
-
- def show
- # Temporary compatibility with CI badges pointing to CI project page
- redirect_to namespace_project_path(project.namespace, project)
- end
-
- # Project status badge
- # Image with build status for sha or ref
- #
- # This action in DEPRECATED, this is here only for backwards compatibility
- # with projects migrated from GitLab CI.
- #
- def badge
- return render_404 unless @project
-
- image = Ci::ImageForBuildService.new.execute(@project, params)
- send_file image.path, filename: image.name, disposition: 'inline', type: "image/svg+xml"
- end
-
- protected
-
- def project
- @project ||= Project.find_by(ci_id: params[:id].to_i)
- end
-
- def no_cache
- response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
- response.headers["Pragma"] = "no-cache"
- response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
- end
-
- def authorize_read_project!
- return access_denied! unless can?(current_user, :read_project, project)
- end
- end
-end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 4c497711fc0..ea441b1736b 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
- return locked_user_redirect(user) if user.access_locked?
+ return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
@@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor
def authenticate_with_two_factor
user = self.resource = find_user
+ return locked_user_redirect(user) unless user.can?(:log_in)
- if user.access_locked?
- locked_user_redirect(user)
- elsif user_params[:otp_attempt].present? && session[:otp_user_id]
+ if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 88d180fcc2e..9ac8197e45a 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -4,10 +4,9 @@ module CreatesCommit
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables
- start_branch = @mr_target_branch unless initial_commit?
commit_params = @commit_params.merge(
start_project: @mr_target_project,
- start_branch: start_branch,
+ start_branch: @mr_target_branch,
target_branch: @mr_source_branch
)
@@ -17,12 +16,16 @@ module CreatesCommit
if result[:status] == :success
update_flash_notice(success_notice)
+ success_path = final_success_path(success_path)
+
respond_to do |format|
- format.html { redirect_to final_success_path(success_path) }
- format.json { render json: { message: "success", filePath: final_success_path(success_path) } }
+ format.html { redirect_to success_path }
+ format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
+ failure_path = failure_path.call if failure_path.respond_to?(:call)
+
respond_to do |format|
format.html do
if failure_view
@@ -58,9 +61,13 @@ module CreatesCommit
end
def final_success_path(success_path)
- return success_path unless create_merge_request?
+ if create_merge_request?
+ merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+ else
+ success_path = success_path.call if success_path.respond_to?(:call)
- merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+ success_path
+ end
end
def new_merge_request_path
@@ -92,46 +99,26 @@ module CreatesCommit
end
def create_merge_request?
- # XXX: Even if the field is set, if we're checking the same branch
+ # Even if the field is set, if we're checking the same branch
# as the target branch in the same project,
# we don't want to create a merge request.
params[:create_merge_request].present? &&
- (different_project? || @ref != @target_branch)
+ (different_project? || @mr_target_branch != @mr_source_branch)
end
- # TODO: We should really clean this up
def set_commit_variables
if can?(current_user, :push_code, @project)
- # Edit file in this project
@mr_source_project = @project
+ @target_branch ||= @ref
else
- # Merge request from fork to this project
@mr_source_project = current_user.fork_of(@project)
+ @target_branch ||= @mr_source_project.repository.next_branch('patch')
end
# Merge request to this project
@mr_target_project = @project
- @mr_target_branch = @ref || @target_branch
-
- @mr_source_branch = guess_mr_source_branch
- end
-
- def initial_commit?
- @mr_target_branch.nil? ||
- !@mr_target_project.repository.branch_exists?(@mr_target_branch)
- end
+ @mr_target_branch ||= @ref || @target_branch
- def guess_mr_source_branch
- # XXX: Happens when viewing a commit without a branch. In this case,
- # @target_branch would be the default branch for @mr_source_project,
- # however we want a generated new branch here. Thus we can't use
- # @target_branch, but should pass nil to indicate that we want a new
- # branch instead of @target_branch.
- return if
- create_merge_request? &&
- # XXX: Don't understand why rubocop prefers this indention
- @mr_source_project.repository.branch_exists?(@target_branch)
-
- @target_branch
+ @mr_source_branch = @target_branch
end
end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
index 586f97c5eb4..6014112256a 100644
--- a/app/controllers/concerns/filter_projects.rb
+++ b/app/controllers/concerns/filter_projects.rb
@@ -8,7 +8,7 @@ module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
- projects = projects.search(params[:filter_projects]) if params[:filter_projects].present?
+ projects = projects.search(params[:name]) if params[:name].present?
projects = projects.non_archived if params[:archived].blank?
projects = projects.personal(current_user) if params[:personal].present? && current_user
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0821974aa93..3ccf2a9ce33 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -26,6 +26,23 @@ module IssuableActions
private
+ def render_conflict_response
+ respond_to do |format|
+ format.html do
+ @conflict = true
+ render :edit
+ end
+
+ format.json do
+ render json: {
+ errors: [
+ "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
+ ]
+ }, status: 409
+ end
+ end
+ end
+
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index a6e158ebae6..85ae4985e58 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,24 +9,32 @@ module IssuableCollections
private
- def issuable_meta_data(issuable_collection)
+ def issuable_meta_data(issuable_collection, collection_type)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
- issuable_ids = issuable_collection.map(&:id)
- issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
+ issuable_ids = issuable_collection.map(&:id)
+ issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
+ issuable_merge_requests_count =
+ if collection_type == 'Issue'
+ MergeRequestsClosingIssues.count_for_collection(issuable_ids)
+ else
+ []
+ end
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
- upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
- notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
+ notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
- notes.try(:count).to_i
+ notes.try(:count).to_i,
+ merge_requests.try(:last).to_i
)
end
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index fb5edb34370..b17c138d5c7 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -10,7 +10,7 @@ module IssuesAction
.page(params[:page])
@collection_type = "Issue"
- @issuable_meta_data = issuable_meta_data(@issues)
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 6229759dcf1..d3c8e4888bc 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -9,7 +9,7 @@ module MergeRequestsAction
.page(params[:page])
@collection_type = "MergeRequest"
- @issuable_meta_data = issuable_meta_data(@merge_requests)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
end
private
diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb
new file mode 100644
index 00000000000..0854c73a02f
--- /dev/null
+++ b/app/controllers/concerns/repository_settings_redirect.rb
@@ -0,0 +1,7 @@
+module RepositorySettingsRedirect
+ extend ActiveSupport::Concern
+
+ def redirect_to_repository_settings(project)
+ redirect_to namespace_project_settings_repository_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index d7f5a4e4682..a8c0937569c 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -33,10 +33,10 @@ module ServiceParams
:issues_url,
:jira_issue_transition_id,
:merge_requests_events,
+ :mock_service_url,
:namespace,
:new_issue_url,
:notify,
- :notify_only_broken_builds,
:notify_only_broken_pipelines,
:password,
:priority,
@@ -59,10 +59,10 @@ module ServiceParams
:user_key,
:username,
:webhook
- ]
+ ].freeze
# Parameters to ignore if no value is specified
- FILTER_BLANK_PARAMS = [:password]
+ FILTER_BLANK_PARAMS = [:password].freeze
def service_params
dynamic_params = @service.event_channel_names + @service.event_names
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index da225d8f1c7..d0a692070d9 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -27,7 +27,7 @@ module SpammableActions
render :verify
else
- fallback.call
+ yield
end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 0b7cf8167f0..d03265e9f20 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,17 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.includes(source: :route).page(params[:page])
+ @group_members = current_user.group_members.includes(source: :route).joins(:group)
+ @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
+ @group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
+ @group_members = @group_members.page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
+ }
+ end
+ end
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 7f506db583f..df528d10f6e 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def index
respond_to do |format|
format.html do
+ @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 325ae565537..be00d765f73 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -42,7 +42,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
- projects = base_scope.sorted_by_activity.includes(:namespace)
+ projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
filter_projects(projects)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 5848ca62777..096de8032ae 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy_all
- TodoService.new.mark_todos_as_done(@todos, current_user)
+ updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
- format.json { render json: todos_counts }
+ format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
end
@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts
end
+ def bulk_restore
+ TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
+
+ render json: todos_counts
+ end
+
# Used in TodosHelper also
def self.todos_count_format(count)
count >= 100 ? '99+' : count
@@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private
def find_todos
- @todos ||= TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
end
def todos_counts
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
deleted file mode 100644
index 1bec5a7d27f..00000000000
--- a/app/controllers/emojis_controller.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class EmojisController < ApplicationController
- layout false
-
- def index
- end
-end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index a962f9a0937..68228c095da 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,8 +1,17 @@
class Explore::GroupsController < Explore::ApplicationController
def index
@groups = GroupsFinder.new.execute(current_user)
- @groups = @groups.search(params[:search]) if params[:search].present?
+ @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups })
+ }
+ end
+ end
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 26e17a7553e..6167f9bd335 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -2,7 +2,7 @@ class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = load_projects
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@@ -21,7 +21,8 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = filter_projects(Project.trending)
+ @projects = load_projects(Project.trending)
+ @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@@ -36,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = load_projects
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
@@ -50,4 +51,11 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
end
+
+ protected
+
+ def load_projects(base_scope = nil)
+ base_scope ||= ProjectsFinder.new.execute(current_user)
+ base_scope.includes(:route, namespace: :route)
+ end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0d872c86c8a..43102596201 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
+ @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 7ed54479599..05f9ee1ee90 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController
@group = Groups::CreateService.new(current_user, group_params).execute
if @group.persisted?
- redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
+ notice = if @group.chat_team.present?
+ "Group '#{@group.name}' and its Mattermost team were successfully created."
+ else
+ "Group '#{@group.name}' was successfully created."
+ end
+
+ redirect_to @group, notice: notice
else
render action: "new"
end
@@ -108,11 +114,11 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]) if params[:name].blank?
end
def authorize_create_group!
- unless can?(current_user, :create_group, nil)
+ unless can?(current_user, :create_group)
return render_404
end
end
@@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled,
:share_with_group_lock,
:visibility_level,
- :parent_id
+ :parent_id,
+ :create_chat_team,
+ :chat_team_name
]
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c2e4d62b50b..3109439b2ff 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -5,7 +5,7 @@ class JwtController < ApplicationController
SERVICES = {
Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService,
- }
+ }.freeze
def auth
service = SERVICES[params[:service]]
@@ -39,7 +39,8 @@ class JwtController < ApplicationController
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
- ] }, status: 401
+ ]
+ }, status: 401
end
def render_unauthorized
@@ -47,7 +48,8 @@ class JwtController < ApplicationController
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
- ] }, status: 401
+ ]
+ }, status: 401
end
def auth_params
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index c721dca58d9..05190103767 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,8 +1,8 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
- before_action :authenticate_resource_owner!
-
layout 'profile'
+ # Overriden from Doorkeeper::AuthorizationsController to
+ # include the call to session.delete
def new
if pre_auth.authorizable?
if skip_authorization? || matching_token?
@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
render "doorkeeper/authorizations/error"
end
end
-
- # TODO: Handle raise invalid authorization
- def create
- redirect_or_render authorization.authorize
- end
-
- def destroy
- redirect_or_render authorization.deny
- end
-
- private
-
- def matching_token?
- Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
- current_resource_owner.id,
- pre_auth.scopes)
- end
-
- def redirect_or_render(auth)
- if auth.redirectable?
- redirect_to auth.redirect_uri
- else
- render json: auth.body, status: auth.status
- end
- end
-
- def pre_auth
- @pre_auth ||=
- Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
- server.client_via_uid,
- params)
- end
-
- def authorization
- @authorization ||= strategy.request
- end
-
- def strategy
- @strategy ||= server.authorization_request(pre_auth.response_type)
- end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index c8663a3c38e..e4452f46056 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -10,11 +10,6 @@ class Profiles::KeysController < Profiles::ApplicationController
@key = current_user.keys.find(params[:id])
end
- # Back-compat: We need to support this URL since git-annex webapp points to it
- def new
- redirect_to profile_keys_path
- end
-
def create
@key = current_user.keys.new(key_params)
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index a271e2dfc4b..b8b71d295f6 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :notified_of_own_activity)
+ params.require(:user).permit(:notification_email)
end
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 6e007f17913..0abe7ea3c9b 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
- @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+ @personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def revoke
- @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+ @personal_access_token = finder.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
+ def finder(options = {})
+ PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options))
+ end
+
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
def set_index_vars
- @personal_access_token ||= current_user.personal_access_tokens.build
- @scopes = Gitlab::Auth::SCOPES
- @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
- @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
+ @scopes = Gitlab::Auth::API_SCOPES
+
+ @personal_access_token = finder.build
+ @inactive_personal_access_tokens = finder(state: 'inactive').execute
+ @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 18044ca78e2..26e7e93533e 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -80,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
- RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
+ RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
end
def account_string
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index f0c71725ea8..987b95e89b9 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController
end
def update_username
- @user.update_attributes(username: user_params[:username])
-
- respond_to do |format|
- format.js
+ if @user.update_attributes(username: user_params[:username])
+ options = { notice: "Username successfully changed" }
+ else
+ message = @user.errors.full_messages.uniq.join('. ')
+ options = { alert: "Username change failed - #{message}" }
end
+
+ redirect_back_or_default(default: { action: 'show' }, options: options)
end
private
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index d9dfa534669..ffb54390965 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,9 +1,5 @@
class Projects::AutocompleteSourcesController < Projects::ApplicationController
- before_action :load_autocomplete_service, except: [:emojis, :members]
-
- def emojis
- render json: Gitlab::AwardEmoji.urls
- end
+ before_action :load_autocomplete_service, except: [:members]
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 863a766a255..6461eeac11c 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
-
+
return render_404 unless @blob
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 39ba815cfca..52fc67d162c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -5,7 +5,7 @@ class Projects::BlobController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
- class InvalidPathError < StandardError; end
+ InvalidPathError = Class.new(StandardError)
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
@@ -23,8 +23,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def create
+ update_ref
+
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)),
+ 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
@@ -40,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController
def update
@path = params[:file_path] if params[:file_path].present?
- create_commit(Files::UpdateService, success_path: after_edit_path,
+ create_commit(Files::UpdateService, success_path: -> { after_edit_path },
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
@@ -62,7 +64,7 @@ class Projects::BlobController < Projects::ApplicationController
def destroy
create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
- success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
+ 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
@@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController
private
+ def update_ref
+ branch_exists = @repository.find_branch(@target_branch)
+ @ref = @target_branch if branch_exists
+ end
+
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 61fef4dc133..28c9646910d 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -8,6 +8,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
+ make_sure_position_is_set(issues)
render json: {
issues: serialize_as_json(issues),
@@ -38,6 +39,12 @@ module Projects
private
+ def make_sure_position_is_set(issues)
+ issues.each do |issue|
+ issue.move_to_end && issue.save unless issue.relative_position
+ end
+ end
+
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id)
@@ -63,7 +70,7 @@ module Projects
end
def move_params
- params.permit(:board_id, :id, :from_list_id, :to_list_id)
+ params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
end
def issue_params
@@ -73,7 +80,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:id, :iid, :title, :confidential, :due_date],
+ only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 89d84809e3a..840405f38cb 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,25 +1,29 @@
class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
include SortingHelper
+
# Authorize
- before_action :require_non_empty_project
+ before_action :require_non_empty_project, except: :create
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
-
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
- end
respond_to do |format|
- format.html
+ format.html do
+ paginate_branches
+ @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
+ end
format.json do
- render json: @repository.branch_names
+ paginate_branches unless params[:show_all]
+ render json: @branches.map(&:name)
end
end
end
@@ -32,6 +36,8 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
+ redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
@@ -42,8 +48,15 @@ class Projects::BranchesController < Projects::ApplicationController
if result[:status] == :success
@branch = result[:branch]
- redirect_to namespace_project_tree_path(@project.namespace, @project,
- @branch.name)
+
+ if redirect_to_autodeploy
+ redirect_to(
+ url_to_autodeploy_setup(project, branch_name),
+ notice: view_context.autodeploy_flash_notice(branch_name))
+ else
+ redirect_to namespace_project_tree_path(@project.namespace, @project,
+ @branch.name)
+ end
else
@error = result[:message]
render action: 'new'
@@ -76,7 +89,23 @@ class Projects::BranchesController < Projects::ApplicationController
ref_escaped = sanitize(strip_tags(params[:ref]))
Addressable::URI.unescape(ref_escaped)
else
- @project.default_branch
+ @project.default_branch || 'master'
end
end
+
+ def paginate_branches
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ end
+
+ def url_to_autodeploy_setup(project, branch_name)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ branch_name,
+ file_name: '.gitlab-ci.yml',
+ commit_message: 'Set up auto deploy',
+ target_branch: branch_name,
+ context: 'autodeploy'
+ )
+ end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index e10d7992db7..cc67f688d51 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -51,23 +51,35 @@ class Projects::CommitController < Projects::ApplicationController
def revert
assign_change_commit_vars
- return render_404 if @target_branch.blank?
+ return render_404 if @start_branch.blank?
+
+ @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch
+
+ @mr_target_branch = @start_branch
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
- success_path: successful_change_path, failure_path: failed_change_path)
+ success_path: -> { successful_change_path }, failure_path: failed_change_path)
end
def cherry_pick
assign_change_commit_vars
- return render_404 if @target_branch.blank?
+ return render_404 if @start_branch.blank?
+
+ @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
+
+ @mr_target_branch = @start_branch
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
- success_path: successful_change_path, failure_path: failed_change_path)
+ success_path: -> { successful_change_path }, failure_path: failed_change_path)
end
private
+ def create_new_branch?
+ params[:create_merge_request].present? || !can?(current_user, :push_code, @project)
+ end
+
def successful_change_path
referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
@@ -78,7 +90,7 @@ class Projects::CommitController < Projects::ApplicationController
def referenced_merge_request_url
if merge_request = @commit.merged_merge_request(current_user)
- namespace_project_merge_request_url(@project.namespace, @project, merge_request)
+ namespace_project_merge_request_url(merge_request.target_project.namespace, merge_request.target_project, merge_request)
end
end
@@ -94,7 +106,7 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
-
+
@environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
@@ -118,11 +130,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def assign_change_commit_vars
- @commit = project.commit(params[:id])
- @target_branch = params[:target_branch]
- @commit_params = {
- commit: @commit,
- create_merge_request: params[:create_merge_request].present? || different_project?
- }
+ @start_branch = params[:start_branch]
+ @commit_params = { commit: @commit }
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index b094491e006..1502b734f37 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -1,4 +1,5 @@
class Projects::DeployKeysController < Projects::ApplicationController
+ include RepositorySettingsRedirect
respond_to :html
# Authorize
@@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
- @key = DeployKey.new
- set_index_vars
+ redirect_to_repository_settings(@project)
end
def new
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+ redirect_to_repository_settings(@project)
end
def create
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
- set_index_vars
- if @key.valid? && @project.deploy_keys << @key
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
- else
- render "index"
+ unless @key.valid? && @project.deploy_keys << @key
+ flash[:alert] = @key.errors.full_messages.join(', ').html_safe
end
+ redirect_to_repository_settings(@project)
end
def enable
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+ redirect_to_repository_settings(@project)
end
def disable
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
- redirect_back_or_default(default: { action: 'index' })
+ redirect_to_repository_settings(@project)
end
protected
- def set_index_vars
- @enabled_keys ||= @project.deploy_keys
-
- @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys
- @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
- @available_public_keys ||= DeployKey.are_public - @enabled_keys
-
- # Public keys that are already used by another accessible project are already
- # in @available_project_keys.
- @available_public_keys -= @available_project_keys
- end
-
def deploy_key_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index fed75396d6e..fa37963dfd4 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
- before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
+ before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def metrics
+ # Currently, this acts as a hint to load the metrics details into the cache
+ # if they aren't there already
+ @metrics = environment.metrics || {}
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @metrics, status: @metrics.any? ? :ok : :no_content
+ end
+ end
+ end
+
private
def verify_api_request!
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 216c158e41e..9a1bf037a95 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -76,11 +76,12 @@ class Projects::GitHttpClientController < Projects::ApplicationController
return @project if defined?(@project)
project_id, _ = project_id_with_suffix
- if project_id.blank?
- @project = nil
- else
- @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
- end
+ @project =
+ if project_id.blank?
+ nil
+ else
+ Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
+ end
end
# This method returns two values so that we can parse
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 923e7340e69..43fc0c39801 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -17,6 +17,25 @@ class Projects::GraphsController < Projects::ApplicationController
end
def commits
+ redirect_to action: 'charts'
+ end
+
+ def languages
+ redirect_to action: 'charts'
+ end
+
+ def charts
+ get_commits
+ get_languages
+ end
+
+ def ci
+ redirect_to charts_namespace_project_pipelines_path(@project.namespace, @project)
+ end
+
+ private
+
+ def get_commits
@commits = @project.repository.commits(@ref, limit: 2000, skip_merges: true)
@commits_graph = Gitlab::Graphs::Commits.new(@commits)
@commits_per_week_days = @commits_graph.commits_per_week_days
@@ -24,15 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController
@commits_per_month = @commits_graph.commits_per_month
end
- def ci
- @charts = {}
- @charts[:week] = Ci::Charts::WeekChart.new(project)
- @charts[:month] = Ci::Charts::MonthChart.new(project)
- @charts[:year] = Ci::Charts::YearChart.new(project)
- @charts[:build_times] = Ci::Charts::BuildTime.new(project)
- end
-
- def languages
+ def get_languages
@languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages
total = @languages.map(&:last).sum
@@ -52,8 +63,6 @@ class Projects::GraphsController < Projects::ApplicationController
end
end
- private
-
def fetch_graph
@commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true)
@log = []
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6ef36771ac1..cdb5b4173d3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include SpammableActions
+ prepend_before_action :authenticate_user!, only: [:new]
+
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -26,7 +28,7 @@ class Projects::IssuesController < Projects::ApplicationController
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues)
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
@@ -64,8 +66,15 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
)
- build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
+ build_params = issue_params.merge(
+ merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+ discussion_to_resolve: params[:discussion_to_resolve]
+ )
+ service = Issues::BuildService.new(project, current_user, build_params)
+
+ @issue = @noteable = service.execute
+ @merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
+ @discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
respond_with(@issue)
end
@@ -94,11 +103,21 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- create_params = issue_params
- .merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- .merge(spammable_params)
+ create_params = issue_params.merge(spammable_params).merge(
+ merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
+ discussion_to_resolve: params[:discussion_to_resolve]
+ )
- @issue = Issues::CreateService.new(project, current_user, create_params).execute
+ service = Issues::CreateService.new(project, current_user, create_params)
+ @issue = service.execute
+
+ if service.discussions_to_resolve.count(&:resolved?) > 0
+ flash[:notice] = if service.discussion_to_resolve_id
+ "Resolved 1 discussion."
+ else
+ "Resolved all discussions."
+ end
+ end
respond_to do |format|
format.html do
@@ -129,13 +148,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
format.json do
- render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def referenced_merge_requests
@@ -186,14 +204,6 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue
alias_method :spammable, :issue
- def merge_request_for_resolving_discussions
- return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
-
- @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
-
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 75971faa93e..677a8a1a73a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,11 +10,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
- :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
+ :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
- before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
+ before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
@@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@merge_requests)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
@@ -245,9 +245,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json do
define_pipelines_vars
- render json: PipelineSerializer
+ render json: {
+ pipelines: PipelineSerializer
.new(project: @project, user: @current_user)
.represent(@pipelines)
+ }
end
end
end
@@ -296,22 +298,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
- if @merge_request.valid?
- respond_to do |format|
- format.html do
- redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
- @merge_request.target_project, @merge_request])
- end
- format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ respond_to do |format|
+ format.html do
+ if @merge_request.valid?
+ redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
+ else
+ render :edit
end
end
- else
- render "edit"
+
+ format.json do
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ end
end
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def remove_wip
@@ -323,12 +324,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_check
@merge_request.check_if_can_be_merged
+ @pipelines = @merge_request.all_pipelines
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
- def cancel_merge_when_build_succeeds
- unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ def cancel_merge_when_pipeline_succeeds
+ unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
return access_denied!
end
@@ -340,9 +342,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
- # Disable the CI check if merge_when_build_succeeds is enabled since we have
+ # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
# to wait until CI completes to know
- unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?)
+ unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
@status = :failed
return
end
@@ -354,7 +356,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
- if params[:merge_when_build_succeeds].present?
+ if params[:merge_when_pipeline_succeeds].present?
unless @merge_request.head_pipeline
@status = :failed
return
@@ -365,7 +367,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
.new(@project, current_user, merge_params)
.execute(@merge_request)
- @status = :merge_when_build_succeeds
+ @status = :merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
@@ -381,14 +383,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
- if merge_request.merge_when_build_succeeds
- @status = :merge_when_build_succeeds
- else
- # Only MRs that can be merged end in this action
- # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
- # in last case it does not have any special status. Possible error is handled inside widget js function
- @status = :success
- end
+ @status =
+ if merge_request.merge_when_pipeline_succeeds
+ :merge_when_pipeline_succeeds
+ else
+ # Only MRs that can be merged end in this action
+ # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
+ # in last case it does not have any special status. Possible error is handled inside widget js function
+ :success
+ end
render 'merge'
end
@@ -444,6 +447,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_status
pipeline = @merge_request.head_pipeline
+ @pipelines = @merge_request.all_pipelines
if pipeline
status = pipeline.status
@@ -462,7 +466,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage,
- pipeline: pipeline.try(:id)
+ pipeline: pipeline.try(:id),
+ has_ci: @merge_request.has_ci?
}
render json: response
@@ -672,8 +677,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.ensure_ref_fetched
end
- def merge_when_build_succeeds_active?
- params[:merge_when_build_succeeds].present? &&
+ def merge_when_pipeline_succeeds_active?
+ params[:merge_when_pipeline_succeeds].present? &&
@merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index b033f7b5ea9..d00177e7612 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -148,17 +148,10 @@ class Projects::NotesController < Projects::ApplicationController
def note_json(note)
attrs = {
- award: false,
id: note.id
}
- if note.is_a?(AwardEmoji)
- attrs.merge!(
- valid: note.valid?,
- award: true,
- name: note.name
- )
- elsif note.persisted?
+ if note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
attrs.merge!(
@@ -198,7 +191,7 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- attrs[:commands_changes] = note.commands_changes unless attrs[:award]
+ attrs[:commands_changes] = note.commands_changes
attrs
end
@@ -218,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController
end
def find_current_user_notes
- @notes = NotesFinder.new(project, current_user, params).execute.inc_author
+ @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
+ .execute.inc_author
+ end
+
+ def last_fetched_at
+ request.headers['X-Last-Fetched-At']
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 84451257b98..718d9e86bea 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,9 +1,10 @@
class Projects::PipelinesController < Projects::ApplicationController
- before_action :pipeline, except: [:index, :new, :create]
+ before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+ before_action :builds_enabled, only: :charts
def index
@scope = params[:scope]
@@ -13,9 +14,15 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
- @running_or_pending_count = PipelinesFinder
+ @running_count = PipelinesFinder
.new(project).execute(scope: 'running').count
+ @pending_count = PipelinesFinder
+ .new(project).execute(scope: 'pending').count
+
+ @finished_count = PipelinesFinder
+ .new(project).execute(scope: 'finished').count
+
@pipelines_count = PipelinesFinder
.new(project).execute.count
@@ -29,7 +36,9 @@ class Projects::PipelinesController < Projects::ApplicationController
.represent(@pipelines),
count: {
all: @pipelines_count,
- running_or_pending: @running_or_pending_count
+ running: @running_count,
+ pending: @pending_count,
+ finished: @finished_count,
}
}
end
@@ -84,6 +93,14 @@ class Projects::PipelinesController < Projects::ApplicationController
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
+ def charts
+ @charts = {}
+ @charts[:week] = Ci::Charts::WeekChart.new(project)
+ @charts[:month] = Ci::Charts::MonthChart.new(project)
+ @charts[:year] = Ci::Charts::YearChart.new(project)
+ @charts[:build_times] = Ci::Charts::BuildTime.new(project)
+ end
+
private
def create_params
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 2f422d352ed..a8cb07eb67a 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,26 +1,22 @@
class Projects::ProtectedBranchesController < Projects::ApplicationController
+ include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
- before_action :load_protected_branches, only: [:index]
layout "project_settings"
def index
- @protected_branch = @project.protected_branches.new
- load_gon_index
+ redirect_to_repository_settings(@project)
end
def create
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
- if @protected_branch.persisted?
- redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
- else
- load_protected_branches
- load_gon_index
- render :index
+ unless @protected_branch.persisted?
+ flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
end
+ redirect_to_repository_settings(@project)
end
def show
@@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
@protected_branch.destroy
respond_to do |format|
- format.html { redirect_to namespace_project_protected_branches_path }
+ format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
@@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
end
-
- def load_protected_branches
- @protected_branches = @project.protected_branches.order(:name).page(params[:page])
- end
-
- def access_levels_options
- {
- push_access_levels: {
- "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
- },
- merge_access_levels: {
- "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
- }
- }
- end
-
- def load_gon_index
- params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
- gon.push(params.merge(access_levels_options))
- end
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 10d24da16d7..c55b37ae0dd 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob?
- if @blob.lfs_pointer?
+ if @blob.lfs_pointer? && project.lfs_enabled?
send_lfs_object
else
send_git_blob @repository, @blob
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 17cb1d5be24..f9d798d0455 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- if @service.update_attributes(service_params[:service])
+ @service.assign_attributes(service_params[:service])
+ if @service.save(context: :manual_change)
redirect_to(
edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
notice: 'Successfully updated.'
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
index 5735e281f66..cbfa2afa959 100644
--- a/app/controllers/projects/settings/members_controller.rb
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -7,47 +7,18 @@ module Projects
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
- @project_members = @project.project_members
- @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
- group = @project.group
-
- # group links
- @group_links = @project.project_group_links.all
-
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
- if group
- # We need `.where.not(user_id: nil)` here otherwise when a group has an
- # invitee, it would make the following query return 0 rows since a NULL
- # user_id would be present in the subquery
- # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
- group_members = MembersFinder.new(@project_members, group).execute(current_user)
- end
+ @project_members = MembersFinder.new(@project, current_user).execute
if params[:search].present?
- user_ids = @project.users.search(params[:search]).select(:id)
- @project_members = @project_members.where(user_id: user_ids)
-
- if group_members
- user_ids = group.users.search(params[:search]).select(:id)
- group_members = group_members.where(user_id: user_ids)
- end
-
- @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+ @project_members = @project_members.joins(:user).merge(User.search(params[:search]))
+ @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
-
- @project_members = Member.
- where(wheres.join(' OR ')).
- sort(@sort).
- page(params[:page])
-
+ @project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
@project_member = @project.project_members.new
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
new file mode 100644
index 00000000000..b6ce4abca45
--- /dev/null
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -0,0 +1,50 @@
+module Projects
+ module Settings
+ class RepositoryController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ def show
+ @deploy_keys = DeployKeysPresenter
+ .new(@project, current_user: current_user)
+
+ define_protected_branches
+ end
+
+ private
+
+ def define_protected_branches
+ load_protected_branches
+ @protected_branch = @project.protected_branches.new
+ load_gon_index
+ end
+
+ def load_protected_branches
+ @protected_branches = @project.protected_branches.order(:name).page(params[:page])
+ end
+
+ def access_levels_options
+ {
+ push_access_levels: {
+ roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
+ { id: id, text: text, before_divider: true }
+ end
+ },
+ merge_access_levels: {
+ roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
+ { id: id, text: text, before_divider: true }
+ end
+ }
+ }
+ end
+
+ def open_branches
+ branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
+ { open_branches: branches }
+ end
+
+ def load_gon_index
+ gon.push(open_branches.merge(access_levels_options))
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 33379659d73..e13f0bde315 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController
@tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page])
- @releases = project.releases.where(tag: @tags.map(&:name))
+ tag_names = @tags.map(&:name)
+ @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
+ @releases = project.releases.where(tag: tag_names)
end
def show
@@ -41,13 +43,27 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
- Tags::DestroyService.new(project, current_user).execute(params[:id])
+ result = Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format|
- format.html do
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+ if result[:status] == :success
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
+ end
+
+ format.js
+ else
+ @error = result[:message]
+
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace, @project),
+ alert: @error
+ end
+
+ format.js do
+ render status: :unprocessable_entity
+ end
end
- format.js
end
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index b2c11ea4156..c47198c5eb6 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,5 +1,8 @@
class Projects::TriggersController < Projects::ApplicationController
before_action :authorize_admin_build!
+ before_action :authorize_manage_trigger!, except: [:index, :create]
+ before_action :authorize_admin_trigger!, only: [:edit, :update]
+ before_action :trigger, only: [:take_ownership, :edit, :update, :destroy]
layout 'project_settings'
@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController
end
def create
- @trigger = project.triggers.new
- @trigger.save
+ @trigger = project.triggers.create(create_params.merge(owner: current_user))
if @trigger.valid?
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
+ flash[:notice] = 'Trigger was created successfully.'
else
- @triggers = project.triggers.select(&:persisted?)
- render action: "show"
+ flash[:alert] = 'You could not create a new trigger.'
+ end
+
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
+ end
+
+ def take_ownership
+ if trigger.update(owner: current_user)
+ flash[:notice] = 'Trigger was re-assigned.'
+ else
+ flash[:alert] = 'You could not take ownership of trigger.'
+ end
+
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
+ end
+
+ def edit
+ end
+
+ def update
+ if trigger.update(update_params)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
+ else
+ render action: "edit"
end
end
def destroy
- trigger.destroy
- flash[:alert] = "Trigger removed"
+ if trigger.destroy
+ flash[:notice] = "Trigger removed."
+ else
+ flash[:alert] = "Could not remove the trigger."
+ end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
+ def authorize_manage_trigger!
+ access_denied! unless can?(current_user, :manage_trigger, trigger)
+ end
+
+ def authorize_admin_trigger!
+ access_denied! unless can?(current_user, :admin_trigger, trigger)
+ end
+
def trigger
- @trigger ||= project.triggers.find(params[:id])
+ @trigger ||= project.triggers.find(params[:id]) || render_404
+ end
+
+ def create_params
+ params.require(:trigger).permit(:description)
+ end
+
+ def update_params
+ params.require(:trigger).permit(:description)
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 2d8064c9878..f210f7e61d2 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,5 +1,3 @@
-require 'project_wiki'
-
class Projects::WikisController < Projects::ApplicationController
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
@@ -47,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id])
+ @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
- if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+ if @page.valid?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index acca821782c..47f7e0b1b28 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -117,7 +117,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:alert] = "Project '#{@project.name}' will be deleted."
+ flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted."
redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
@@ -267,8 +267,9 @@ class ProjectsController < Projects::ApplicationController
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
- @issues = issues_collection
- @issues = @issues.page(params[:page])
+ @issues = issues_collection.page(params[:page])
+ @collection_type = 'Issue'
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end
render :show
@@ -314,7 +315,8 @@ class ProjectsController < Projects::ApplicationController
:name,
:namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
- :only_allow_merge_if_build_succeeds,
+ :only_allow_merge_if_pipeline_succeeds,
+ :printing_merge_request_link_enabled,
:path,
:public_builds,
:request_access_enabled,
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index db2817fadf6..1b4545e4a49 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -8,7 +8,9 @@
# `DashboardController#show`, which is the default.
class RootController < Dashboard::ProjectsController
skip_before_action :authenticate_user!, only: [:index]
- before_action :redirect_to_custom_dashboard, only: [:index]
+
+ before_action :redirect_unlogged_user, if: -> { current_user.nil? }
+ before_action :redirect_logged_user, if: -> { current_user.present? }
def index
super
@@ -16,23 +18,38 @@ class RootController < Dashboard::ProjectsController
private
- def redirect_to_custom_dashboard
- return redirect_to new_user_session_path unless current_user
+ def redirect_unlogged_user
+ if redirect_to_home_page_url?
+ redirect_to(current_application_settings.home_page_url)
+ else
+ redirect_to(new_user_session_path)
+ end
+ end
+ def redirect_logged_user
case current_user.dashboard
when 'stars'
flash.keep
- redirect_to starred_dashboard_projects_path
+ redirect_to(starred_dashboard_projects_path)
when 'project_activity'
- redirect_to activity_dashboard_path
+ redirect_to(activity_dashboard_path)
when 'starred_project_activity'
- redirect_to activity_dashboard_path(filter: 'starred')
+ redirect_to(activity_dashboard_path(filter: 'starred'))
when 'groups'
- redirect_to dashboard_groups_path
+ redirect_to(dashboard_groups_path)
when 'todos'
- redirect_to dashboard_todos_path
- else
- return
+ redirect_to(dashboard_todos_path)
end
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
+ return false unless current_application_settings.home_page_url.present?
+
+ home_page_url = current_application_settings.home_page_url.chomp('/')
+ root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
+
+ root_urls.exclude?(home_page_url)
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 93a180b9036..7d81c96262f 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -15,11 +15,12 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- if Gitlab.config.ldap.enabled
- @ldap_servers = Gitlab::LDAP::Config.servers
- else
- @ldap_servers = []
- end
+ @ldap_servers =
+ if Gitlab.config.ldap.enabled
+ Gitlab::LDAP::Config.servers
+ else
+ []
+ end
super
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2d26718873f..f3fd3da8b20 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -28,8 +28,9 @@ class SnippetsController < ApplicationController
@snippets = SnippetsFinder.new.execute(current_user, {
filter: :by_user,
user: @user,
- scope: params[:scope] }).
- page(params[:page])
+ scope: params[:scope]
+ })
+ .page(params[:page])
render 'index'
else
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 509f4f412ca..f1bfd574f04 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
end
disposition = uploader.image? ? 'inline' : 'attachment'
+
+ expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 9f2206346ce..fce3775f40e 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -1,4 +1,4 @@
-class GroupMembersFinder < Projects::ApplicationController
+class GroupMembersFinder
def initialize(group)
@group = group
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 206c92fe82a..f7ebb1807d7 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,7 +19,7 @@
# iids: integer[]
#
class IssuableFinder
- NONE = '0'
+ NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -33,15 +33,17 @@ class IssuableFinder
items = by_scope(items)
items = by_state(items)
items = by_group(items)
- items = by_project(items)
items = by_search(items)
- items = by_milestone(items)
items = by_assignee(items)
items = by_author(items)
- items = by_label(items)
items = by_due_date(items)
items = by_non_archived(items)
items = by_iids(items)
+ items = by_milestone(items)
+ items = by_label(items)
+
+ # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
+ items = by_project(items)
sort(items)
end
@@ -107,8 +109,7 @@ class IssuableFinder
@project = project
end
- def projects
- return @projects if defined?(@projects)
+ def projects(items = nil)
return @projects = project if project?
projects =
@@ -117,7 +118,7 @@ class IssuableFinder
elsif group
GroupProjectsFinder.new(group).execute(current_user)
else
- ProjectsFinder.new.execute(current_user)
+ projects_finder.execute(current_user, item_project_ids(items))
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -257,9 +258,9 @@ class IssuableFinder
def by_project(items)
items =
if project?
- items.of_projects(projects).references_project
- elsif projects
- items.merge(projects.reorder(nil)).join_project
+ items.of_projects(projects(items)).references_project
+ elsif projects(items)
+ items.merge(projects(items).reorder(nil)).join_project
else
items.none
end
@@ -309,18 +310,25 @@ class IssuableFinder
params[:milestone_title] == Milestone::Upcoming.name
end
+ def filter_by_started_milestone?
+ params[:milestone_title] == Milestone::Started.name
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
- upcoming_ids = Milestone.upcoming_ids_by_projects(projects)
+ upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
+ elsif filter_by_started_milestone?
+ items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else
items = items.with_milestone(params[:milestone_title])
+ items_projects = projects(items)
- if projects
- items = items.where(milestones: { project_id: projects })
+ if items_projects
+ items = items.where(milestones: { project_id: items_projects })
end
end
end
@@ -334,9 +342,10 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
+ items_projects = projects(items)
- if projects
- label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
+ if items_projects
+ label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids })
end
end
@@ -396,4 +405,8 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
+
+ def projects_finder
+ @projects_finder ||= ProjectsFinder.new
+ end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index f542f72a386..08713272947 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
+
+ def item_project_ids(items)
+ items&.reorder(nil)&.select(:project_id)
+ end
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 702944404f5..af24045886e 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -1,13 +1,35 @@
-class MembersFinder < Projects::ApplicationController
- def initialize(project_members, project_group)
- @project_members = project_members
- @project_group = project_group
+class MembersFinder
+ attr_reader :project, :current_user, :group
+
+ def initialize(project, current_user)
+ @project = project
+ @current_user = current_user
+ @group = project.group
+ end
+
+ def execute
+ project_members = project.project_members
+ project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
+ wheres = ["members.id IN (#{project_members.select(:id).to_sql})"]
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id)
+
+ group_members = GroupMembersFinder.new(group).execute
+ group_members = group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, group)
+
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})"
+ end
+
+ Member.where(wheres.join(' OR '))
end
- def execute(current_user)
- non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
- group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
- group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
- group_members
+ def can?(*args)
+ Ability.allowed?(*args)
end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b76ca389f38..1eec45d9cb5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+
+ private
+
+ def item_project_ids(items)
+ items&.reorder(nil)&.select(:target_project_id)
+ end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 4bd8c83081a..6630c6384f2 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -28,11 +28,12 @@ class NotesFinder
private
def init_collection
- if @params[:target_id]
- @notes = on_target(@params[:target_type], @params[:target_id])
- else
- @notes = notes_of_any_type
- end
+ @notes =
+ if @params[:target_id]
+ on_target(@params[:target_type], @params[:target_id])
+ else
+ notes_of_any_type
+ end
end
def notes_of_any_type
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
new file mode 100644
index 00000000000..760166b453f
--- /dev/null
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -0,0 +1,45 @@
+class PersonalAccessTokensFinder
+ attr_accessor :params
+
+ delegate :build, :find, :find_by, to: :execute
+
+ def initialize(params = {})
+ @params = params
+ end
+
+ def execute
+ tokens = PersonalAccessToken.all
+ tokens = by_user(tokens)
+ tokens = by_impersonation(tokens)
+ by_state(tokens)
+ end
+
+ private
+
+ def by_user(tokens)
+ return tokens unless @params[:user]
+ tokens.where(user: @params[:user])
+ end
+
+ def by_impersonation(tokens)
+ case @params[:impersonation]
+ when true
+ tokens.with_impersonation
+ when false
+ tokens.without_impersonation
+ else
+ tokens
+ end
+ end
+
+ def by_state(tokens)
+ case @params[:state]
+ when 'active'
+ tokens.active
+ when 'inactive'
+ tokens.inactive
+ else
+ tokens
+ end
+ end
+end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 32aea75486d..a9172f6767f 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -10,7 +10,11 @@ class PipelinesFinder
scoped_pipelines =
case scope
when 'running'
- pipelines.running_or_pending
+ pipelines.running
+ when 'pending'
+ pipelines.pending
+ when 'finished'
+ pipelines.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index a93a63bdb9b..13d33a1c31b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -13,7 +13,7 @@
#
class TodosFinder
- NONE = '0'
+ NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -24,6 +24,7 @@ class TodosFinder
def execute
items = current_user.todos
+ items = include_associations(items)
items = by_action_id(items)
items = by_action(items)
items = by_author(items)
@@ -38,6 +39,17 @@ class TodosFinder
private
+ def include_associations(items)
+ return items unless params[:include_associations]
+
+ items.includes(
+ [
+ target: { project: [:route, namespace: :route] },
+ author: { namespace: :route },
+ ]
+ )
+ end
+
def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end
@@ -99,7 +111,7 @@ class TodosFinder
end
def type?
- type.present? && ['Issue', 'MergeRequest'].include?(type)
+ type.present? && %w(Issue MergeRequest).include?(type)
end
def type
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6db813d4a02..a3213581498 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -69,11 +69,12 @@ module ApplicationHelper
end
def avatar_icon(user_or_email = nil, size = nil, scale = 2)
- if user_or_email.is_a?(User)
- user = user_or_email
- else
- user = User.find_by_any_email(user_or_email.try(:downcase))
- end
+ user =
+ if user_or_email.is_a?(User)
+ user_or_email
+ else
+ User.find_by_any_email(user_or_email.try(:downcase))
+ end
if user
user.avatar_url(size) || default_avatar
@@ -166,7 +167,7 @@ module ApplicationHelper
css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank?
- element = content_tag :time, time.to_s,
+ element = content_tag :time, time.strftime("%b %d, %Y"),
class: css_classes,
title: time.to_time.in_time_zone.to_s(:medium),
datetime: time.to_time.getutc.iso8601,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 60485160495..ca326dd0627 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,28 +1,15 @@
module ApplicationSettingsHelper
- def gravatar_enabled?
- current_application_settings.gravatar_enabled?
- end
-
- def signup_enabled?
- current_application_settings.signup_enabled?
- end
-
- def signin_enabled?
- current_application_settings.signin_enabled?
- end
+ delegate :gravatar_enabled?,
+ :signup_enabled?,
+ :signin_enabled?,
+ :akismet_enabled?,
+ :koding_enabled?,
+ to: :current_application_settings
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
- def askimet_enabled?
- current_application_settings.akismet_enabled?
- end
-
- def koding_enabled?
- current_application_settings.koding_enabled?
- end
-
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
@@ -94,8 +81,8 @@ module ApplicationSettingsHelper
end
def repository_storages_options_for_select
- options = Gitlab.config.repositories.storages.map do |name, path|
- ["#{name} - #{path}", name]
+ options = Gitlab.config.repositories.storages.map do |name, storage|
+ ["#{name} - #{storage['path']}", name]
end
options_for_select(options, @application_setting.repository_storages)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 311a70725ab..8631bc54509 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -153,16 +153,17 @@ module BlobHelper
# Because we are opionated we set the cache headers ourselves.
response.cache_control[:public] = @project.public?
- if @ref && @commit && @ref == @commit.id
- # This is a link to a commit by its commit SHA. That means that the blob
- # is immutable. The only reason to invalidate the cache is if the commit
- # was deleted or if the user lost access to the repository.
- response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
- else
- # A branch or tag points at this blob. That means that the expected blob
- # value may change over time.
- response.cache_control[:max_age] = Blob::CACHE_TIME
- end
+ response.cache_control[:max_age] =
+ if @ref && @commit && @ref == @commit.id
+ # This is a link to a commit by its commit SHA. That means that the blob
+ # is immutable. The only reason to invalidate the cache is if the commit
+ # was deleted or if the user lost access to the repository.
+ Blob::CACHE_TIME_IMMUTABLE
+ else
+ # A branch or tag points at this blob. That means that the expected blob
+ # value may change over time.
+ Blob::CACHE_TIME
+ end
response.etag = @blob.id
!stale
@@ -202,4 +203,18 @@ module BlobHelper
'blob-language' => @blob && @blob.language.try(:ace_mode)
}
end
+
+ def copy_file_path_button(file_path)
+ clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+ end
+
+ def copy_blob_content_button(blob)
+ return if markup?(blob.name)
+
+ clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
+ end
+
+ def open_raw_file_button(path)
+ link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
+ end
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index ff937b5ebd2..2fcb7a59fc3 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -12,7 +12,14 @@ module BuildsHelper
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
- log_state: @build.trace_with_state[:state].to_s
+ log_state: ''
+ }
+ end
+
+ def build_failed_issue_options
+ {
+ title: "Build Failed ##{@build.id}",
+ description: namespace_project_build_url(@project.namespace, @project, @build)
}
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 4c7c16d694c..0b30471f2ae 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -19,7 +19,7 @@ module ButtonHelper
title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
- icon('clipboard'),
+ icon('clipboard', 'aria-hidden': 'true'),
class: "btn #{css_class}",
data: data,
type: :button,
@@ -34,7 +34,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol,
class: klass,
- href: (project.http_url_to_repo if append_link),
+ href: (project.http_url_to_repo(current_user) if append_link),
data: {
html: true,
placement: placement,
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 94f3b480178..2de9e0de310 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -15,6 +15,8 @@ module CiStatusHelper
'passed'
when 'success_with_warnings'
'passed with warnings'
+ when 'manual'
+ 'waiting for manual action'
else
status
end
@@ -48,6 +50,8 @@ module CiStatusHelper
'icon_status_created'
when 'skipped'
'icon_status_skipped'
+ when 'manual'
+ 'icon_status_manual'
else
'icon_status_canceled'
end
@@ -55,6 +59,24 @@ module CiStatusHelper
custom_icon(icon_name)
end
+ def pipeline_status_cache_key(pipeline_status)
+ "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
+ end
+
+ def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
+ project = pipeline_status.project
+ path = pipelines_namespace_project_commit_path(
+ project.namespace,
+ project,
+ pipeline_status.sha)
+
+ render_status_with_link(
+ 'commit',
+ pipeline_status.status,
+ path,
+ tooltip_placement: tooltip_placement)
+ end
+
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
path = pipelines_namespace_project_commit_path(
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 8aad39e148b..cef624430da 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -211,7 +211,7 @@ module CommitsHelper
external_url = environment.external_url_for(diff_new_path, commit_sha)
return unless external_url
- link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link')
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 2843ad96efa..f927cfc998f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,4 +1,6 @@
module EmailsHelper
+ include AppearancesHelper
+
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
@@ -22,7 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
- ["merge_requests", "issues", "commit"].each do |action|
+ %w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
end
@@ -49,4 +51,19 @@ module EmailsHelper
msg = "This link is valid for #{password_reset_token_valid_time}. "
msg << "After it expires, you can #{link_tag}."
end
+
+ def header_logo
+ if brand_item && brand_item.header_logo?
+ image_tag(
+ brand_item.header_logo,
+ style: 'height: 50px'
+ )
+ else
+ image_tag(
+ image_url('mailers/gitlab_header_logo.gif'),
+ size: "55x50",
+ alt: "GitLab"
+ )
+ end
+ end
end
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
new file mode 100644
index 00000000000..482f68f412b
--- /dev/null
+++ b/app/helpers/emoji_helper.rb
@@ -0,0 +1,5 @@
+module EmojiHelper
+ def emoji_icon(*args)
+ raw Gitlab::Emoji.gl_emoji_tag(*args)
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 362046c0270..fb872a13f74 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -162,7 +162,12 @@ module EventsHelper
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
- sanitize(text, tags: %w(a img b pre code p span))
+
+ sanitize(
+ text,
+ tags: %w(a img gl-emoji b pre code p span),
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version']
+ )
end
def event_commit_title(message)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 2b1f3825adc..7bd212a3ef9 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,20 +1,33 @@
module ExploreHelper
def filter_projects_path(options = {})
exist_opts = {
- sort: params[:sort],
+ sort: params[:sort] || @sort,
scope: params[:scope],
group: params[:group],
tag: params[:tag],
visibility_level: params[:visibility_level],
+ name: params[:name],
+ personal: params[:personal],
+ archived: params[:archived],
+ shared: params[:shared],
+ namespace_id: params[:namespace_id],
}
- options = exist_opts.merge(options)
- path = request.path
- path << "?#{options.to_param}"
- path
+ options = exist_opts.merge(options).delete_if { |key, value| value.blank? }
+ request_path_with_options(options)
+ end
+
+ def filter_groups_path(options = {})
+ request_path_with_options(options)
end
def explore_controller?
controller.class.name.split("::").first == "Explore"
end
+
+ private
+
+ def request_path_with_options(options = {})
+ request.path + "?#{options.to_param}"
+ end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 6d365ea9251..cd442237086 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false.
def truncate_if_block(node, truncated)
- if node.element? && node.description.block? && !truncated
+ return true if truncated
+
+ if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling
true
else
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f16a63e2178..e9b7cbbad6a 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -74,6 +74,10 @@ module GitlabRoutingHelper
namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end
+ def environment_metrics_path(environment, *args)
+ metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 926c9703628..a6014088e92 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,17 +12,18 @@ module GroupsHelper
end
def group_title(group, name = nil, url = nil)
+ @has_group_title = true
full_title = ''
group.ancestors.each do |parent|
- full_title += link_to(simple_sanitize(parent.name), group_path(parent))
- full_title += ' / '.html_safe
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
+ full_title += '<span class="hidable"> / </span>'.html_safe
end
- full_title += link_to(simple_sanitize(group.name), group_path(group))
- full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
+ full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path')
+ full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
- content_tag :span do
+ content_tag :span, class: 'group-title' do
full_title.html_safe
end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a0642a1894b..a57b5a8fea5 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -7,7 +7,7 @@ module ImportHelper
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace)
- link_to path_with_namespace, url, target: '_blank'
+ link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end
private
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03354c235eb..a777db2826b 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -1,6 +1,8 @@
module IssuablesHelper
+ include GitlabRoutingHelper
+
def sidebar_gutter_toggle_icon
- sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right')
+ sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' })
end
def sidebar_gutter_collapsed_class
@@ -23,7 +25,7 @@ module IssuablesHelper
def issuable_json_path(issuable)
project = issuable.project
- if issuable.kind_of?(MergeRequest)
+ if issuable.is_a?(MergeRequest)
namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
else
namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
@@ -52,7 +54,7 @@ module IssuablesHelper
field_name: 'issuable_template',
selected: selected_template(issuable),
project_path: ref_project.path,
- namespace_path: ref_project.namespace.path
+ namespace_path: ref_project.namespace.full_path
}
}
@@ -88,15 +90,33 @@ module IssuablesHelper
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
- if milestone_title == Milestone::Upcoming.name
- milestone_title = Milestone::Upcoming.title
- end
+ title =
+ case milestone_title
+ when Milestone::Upcoming.name then Milestone::Upcoming.title
+ when Milestone::Started.name then Milestone::Started.title
+ else milestone_title.presence
+ end
- h(milestone_title.presence || default_label)
+ h(title || default_label)
+ end
+
+ def to_url_reference(issuable)
+ case issuable
+ when Issue
+ link_to issuable.to_reference, issue_url(issuable)
+ when MergeRequest
+ link_to issuable.to_reference, merge_request_url(issuable)
+ else
+ issuable.to_reference
+ end
end
def issuable_meta(issuable, project, text)
- output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
+ output = content_tag(:strong, class: "identifier") do
+ concat("#{text} ")
+ concat(to_url_reference(issuable))
+ end
+
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
@@ -198,7 +218,7 @@ module IssuablesHelper
@counts[issuable_type][state]
end
- IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+ IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index a2d21b67a77..6978b0c89fd 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -87,34 +87,6 @@ module IssuesHelper
icon('eye-slash') if issue.confidential?
end
- def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
- unicode ||= Gitlab::Emoji.emoji_filename(name) rescue ""
-
- data = {
- aliases: aliases.join(" "),
- emoji: name,
- unicode_name: unicode
- }
-
- if sprite
- # Emoji icons for the emoji menu, these use a spritesheet.
- content_tag :div, "",
- class: "icon emoji-icon emoji-#{unicode}",
- title: name,
- data: data
- else
- # Emoji icons displayed separately, used for the awards already given
- # to an issue or merge request.
- content_tag :img, "",
- class: "icon emoji",
- title: name,
- height: "20px",
- width: "20px",
- src: url_to_image("#{unicode}.png"),
- data: data
- end
- end
-
def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
@@ -162,6 +134,20 @@ module IssuesHelper
options_from_collection_for_select(options, 'name', 'title', params[:due_date])
end
+ def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
+ link_text = merge_request.to_reference
+ link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
+
+ path = if single_discussion
+ Gitlab::UrlBuilder.build(single_discussion.first_note)
+ else
+ project = merge_request.project
+ namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ link_to link_text, path
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 320dd89c9d3..68c09c922a6 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -2,6 +2,7 @@ module JavascriptHelper
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js)
end
+
def page_specific_javascript_bundle_tag(js)
javascript_include_tag(*webpack_asset_paths(js))
end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
index 49ac12db832..27ff4051c8d 100644
--- a/app/helpers/mattermost_helper.rb
+++ b/app/helpers/mattermost_helper.rb
@@ -1,9 +1,7 @@
module MattermostHelper
def mattermost_teams_options(teams)
- teams_options = teams.map do |id, options|
- [options['display_name'] || options['name'], id]
+ teams.map do |team|
+ [team['display_name'] || team['name'], team['id']]
end
-
- teams_options.compact.unshift(['Select team...', '0'])
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 7d8505d704e..38be073c8dc 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -146,7 +146,7 @@ module MergeRequestsHelper
def merge_params(merge_request)
{
- merge_when_build_succeeds: true,
+ merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
sha: merge_request.diff_head_sha
}.merge(merge_params_ee(merge_request))
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 729928ce1dd..5053b937c02 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -82,12 +82,13 @@ module MilestonesHelper
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
- elsif milestone.due_date
- days = milestone.remaining_days
- content = content_tag(:strong, days)
- content << " #{'day'.pluralize(days)} remaining"
elsif milestone.upcoming?
content_tag(:strong, 'Upcoming')
+ elsif milestone.due_date
+ time_ago = time_ago_in_words(milestone.due_date)
+ content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+ content.slice!("about ")
+ content << " remaining"
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
@@ -97,7 +98,7 @@ module MilestonesHelper
def milestone_date_range(milestone)
if milestone.start_date && milestone.due_date
- "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
+ "#{milestone.start_date.to_s(:medium)}–#{milestone.due_date.to_s(:medium)}"
elsif milestone.due_date
if milestone.due_date.past?
"expired on #{milestone.due_date.to_s(:medium)}"
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index dc5ae8edbb2..2e3a15bc1b9 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -33,7 +33,7 @@ module NamespacesHelper
end
def namespace_icon(namespace, size = 40)
- if namespace.kind_of?(Group)
+ if namespace.is_a?(Group)
group_icon(namespace)
else
avatar_icon(namespace.owner.email, size)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf..a8f167cbff2 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -16,6 +16,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
+ current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded"
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c3a08d76318..243ef39ef61 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -35,9 +35,8 @@ module PreferencesHelper
def project_view_choices
[
- ['Readme (default)', :readme],
- ['Activity view', :activity],
- ['Files view', :files]
+ ['Files and Readme (default)', :files],
+ ['Activity', :activity]
]
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb98204285d..bd0c2cd661e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -150,6 +150,22 @@ module ProjectsHelper
).html_safe
end
+ def link_to_autodeploy_doc
+ link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank'
+ end
+
+ def autodeploy_flash_notice(branch_name)
+ "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \
+ choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe
+ end
+
+ def project_list_cache_key(project)
+ key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3']
+ key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
+
+ key
+ end
+
private
def repo_children_classes(field)
@@ -232,7 +248,7 @@ module ProjectsHelper
when 'ssh'
project.ssh_url_to_repo
else
- project.http_url_to_repo
+ project.http_url_to_repo(current_user)
end
end
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
new file mode 100644
index 00000000000..ea5d2932ef4
--- /dev/null
+++ b/app/helpers/rss_helper.rb
@@ -0,0 +1,5 @@
+module RssHelper
+ def rss_url_options
+ { format: :atom, private_token: current_user.try(:private_token) }
+ end
+end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index ff787fb4131..959ee310867 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -16,7 +16,8 @@ module SortingHelper
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
sort_value_upvotes => sort_title_upvotes,
- sort_value_priority => sort_title_priority
+ sort_value_priority => sort_title_priority,
+ sort_value_label_priority => sort_title_label_priority
}
end
@@ -30,7 +31,7 @@ module SortingHelper
}
if current_controller?('admin/projects')
- options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+ options[sort_value_largest_repo] = sort_title_largest_repo
end
options
@@ -53,6 +54,10 @@ module SortingHelper
'Priority'
end
+ def sort_title_label_priority
+ 'Label priority'
+ end
+
def sort_title_oldest_updated
'Oldest updated'
end
@@ -161,6 +166,10 @@ module SortingHelper
'priority'
end
+ def sort_value_label_priority
+ 'label_priority'
+ end
+
def sort_value_oldest_updated
'updated_asc'
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 9a748aaaf33..fb95f2b565e 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -37,8 +37,8 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
- return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git' ].join('')
+ return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
+ project, '.git'].join('')
url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
@@ -48,8 +48,8 @@ module SubmoduleHelper
end
def standard_links(host, namespace, project, commit)
- base = [ 'https://', host, '/', namespace, '/', project ].join('')
- [base, [ base, '/tree/', commit ].join('')]
+ base = ['https://', host, '/', namespace, '/', project].join('')
+ [base, [base, '/tree/', commit].join('')]
end
def relative_self_links(url, commit)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 547f6258909..1a55ee05996 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -99,7 +99,7 @@ module TabHelper
return 'active'
end
- if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
+ if %w(services hooks deploy_keys protected_branches).include? controller.controller_name
"active"
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index c52afd6db1c..847a8fdfca6 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -39,9 +39,13 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
- path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
-
- path.unshift(:pipelines) if todo.build_failed?
+ if todo.build_failed?
+ # associated namespace and route would be loaded from the db again if todo.project was used
+ project = todo.target.project
+ path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
+ else
+ path = [todo.target]
+ end
polymorphic_path(path, anchor: anchor)
end
@@ -99,8 +103,7 @@ module TodosHelper
end
def todo_projects_options
- projects = current_user.authorized_projects.sorted_by_activity.non_archived
- projects = projects.includes(:namespace)
+ projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
{ id: project.id, text: project.name_with_namespace }
@@ -150,6 +153,6 @@ module TodosHelper
private
def show_todo_state?(todo)
- (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state)
+ (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index b0135ea2e95..a48d4475e97 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,9 +1,9 @@
module TriggersHelper
def builds_trigger_url(project_id, ref: nil)
if ref.nil?
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline"
else
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+ "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline"
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index fc93acfe63e..169cedeb796 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -89,13 +89,9 @@ module VisibilityLevelHelper
current_application_settings.restricted_visibility_levels || []
end
- def default_project_visibility
- current_application_settings.default_project_visibility
- end
-
- def default_group_visibility
- current_application_settings.default_group_visibility
- end
+ delegate :default_project_visibility,
+ :default_group_visibility,
+ to: :current_application_settings
def skip_level?(form_model, level)
form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201a..00000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
- module Builds
- def build_fail_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('failed')
-
- mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
- end
-
- def build_success_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('success')
- mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
- end
-
- private
-
- def add_build_headers(status)
- headers['X-GitLab-Build-Id'] = @build.id
- headers['X-GitLab-Build-Ref'] = @build.ref
- headers['X-GitLab-Build-Status'] = status.to_s
- end
- end
-end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 9460a6cd2be..f9f45ab987b 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -22,8 +22,8 @@ module Emails
mail(bcc: recipients,
subject: pipeline_subject(status),
skip_premailer: true) do |format|
- format.html { render layout: false }
- format.text
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81..14df6f8f0a3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Builds
include Emails::Pipelines
include Emails::Members
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 21db2fe04a0..22a9f5da646 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -1,10 +1,11 @@
class RepositoryCheckMailer < BaseMailer
def notify(failed_count)
- if failed_count == 1
- @message = "One project failed its last repository check"
- else
- @message = "#{failed_count} projects failed their last repository check"
- end
+ @message =
+ if failed_count == 1
+ "One project failed its last repository check"
+ else
+ "#{failed_count} projects failed their last repository check"
+ end
mail(
to: User.admins.pluck(:email),
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ad6c588202e..f3692a5a067 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -56,15 +56,16 @@ class Ability
end
end
- def allowed?(user, action, subject)
+ def allowed?(user, action, subject = :global)
allowed(user, subject).include?(action)
end
- def allowed(user, subject)
+ def allowed(user, subject = :global)
+ return BasePolicy::RuleSet.none if subject.nil?
return uncached_allowed(user, subject) unless RequestStore.active?
user_key = user ? user.id : 'anonymous'
- subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+ subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}"
key = "/ability/#{user_key}/#{subject_key}"
RequestStore[key] ||= uncached_allowed(user, subject).freeze
end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index e4106e1c2e9..c79326e8427 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
+ has_many :uploads, as: :model, dependent: :destroy
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b94a71e1ea7..9d01a70c77d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -6,7 +6,7 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :health_check_access_token
add_authentication_token_field :container_registry_access_token
- CACHE_KEY = 'application_setting.last'
+ CACHE_KEY = 'application_setting.last'.freeze
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
@@ -65,6 +65,16 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
+ validates :unique_ips_limit_per_user,
+ numericality: { greater_than_or_equal_to: 1 },
+ presence: true,
+ if: :unique_ips_limit_enabled
+
+ validates :unique_ips_limit_time_window,
+ numericality: { greater_than_or_equal_to: 0 },
+ presence: true,
+ if: :unique_ips_limit_enabled
+
validates :koding_url,
presence: true,
if: :koding_enabled
@@ -77,6 +87,12 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :max_artifacts_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :default_artifacts_expire_in, presence: true, duration: true
+
validates :container_registry_token_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -149,6 +165,8 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.current
+ ensure_cache_setup
+
Rails.cache.fetch(CACHE_KEY) do
ApplicationSetting.last
end
@@ -162,22 +180,34 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.cached
+ ensure_cache_setup
Rails.cache.fetch(CACHE_KEY)
end
+ def self.ensure_cache_setup
+ # This is a workaround for a Rails bug that causes attribute methods not
+ # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+ ApplicationSetting.define_attribute_methods
+ end
+
def self.defaults_ce
{
after_sign_up_text: nil,
akismet_enabled: false,
container_registry_token_expire_delay: 5,
+ default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
+ unique_ips_limit_per_user: 10,
+ unique_ips_limit_time_window: 3600,
+ unique_ips_limit_enabled: false,
housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true,
housekeeping_full_repack_period: 50,
@@ -203,9 +233,9 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
+ terminal_max_session_time: 0,
two_factor_grace_period: 48,
- user_default_external: false,
- terminal_max_session_time: 0
+ user_default_external: false
}
end
@@ -217,6 +247,14 @@ class ApplicationSetting < ActiveRecord::Base
create(defaults)
end
+ def self.human_attribute_name(attr, _options = {})
+ if attr == :default_artifacts_expire_in
+ 'Default artifacts expiration'
+ else
+ super
+ end
+ end
+
def home_page_url_column_exist
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
@@ -264,6 +302,22 @@ class ApplicationSetting < ActiveRecord::Base
self.repository_storages = [value]
end
+ def default_project_visibility=(level)
+ super(Gitlab::VisibilityLevel.level_value(level))
+ end
+
+ def default_snippet_visibility=(level)
+ super(Gitlab::VisibilityLevel.level_value(level))
+ end
+
+ def default_group_visibility=(level)
+ super(Gitlab::VisibilityLevel.level_value(level))
+ end
+
+ def restricted_visibility_levels=(levels)
+ super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
+ end
+
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
diff --git a/app/models/blob.rb b/app/models/blob.rb
index ab92e820335..1376b86fdad 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -54,9 +54,13 @@ class Blob < SimpleDelegator
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
end
- def to_partial_path
+ def to_partial_path(project)
if lfs_pointer?
- 'download'
+ if project.lfs_enabled?
+ 'download'
+ else
+ 'text'
+ end
elsif image? || svg?
'image'
elsif text?
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
new file mode 100644
index 00000000000..c52b6f15913
--- /dev/null
+++ b/app/models/chat_team.rb
@@ -0,0 +1,6 @@
+class ChatTeam < ActiveRecord::Base
+ validates :team_id, presence: true
+ validates :namespace, uniqueness: true
+
+ belongs_to :namespace
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e018f8e7c4e..ad0be70c32a 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,15 +15,17 @@ module Ci
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
- project_id: gl_project_id
+ project: project
)
end
serialize :options
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
+ delegate :name, to: :project, prefix: true
+
validates :coverage, numericality: true, allow_blank: true
- validates_presence_of :ref
+ validates :ref, presence: true
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
@@ -53,15 +55,6 @@ module Ci
pending.unstarted.order('created_at ASC').first
end
- def create_from(build)
- new_build = build.dup
- new_build.status = 'pending'
- new_build.runner_id = nil
- new_build.trigger_request_id = nil
- new_build.token = nil
- new_build.save
- end
-
def retry(build, current_user)
Ci::RetryBuildService
.new(build.project, current_user)
@@ -70,6 +63,10 @@ module Ci
end
state_machine :status do
+ event :actionize do
+ transition created: :manual
+ end
+
after_transition any => [:pending] do |build|
build.run_after_commit do
BuildQueueWorker.perform_async(id)
@@ -101,16 +98,21 @@ module Ci
.fabricate!
end
- def manual?
- self.when == 'manual'
- end
-
def other_actions
pipeline.manual_actions.where.not(name: name)
end
def playable?
- project.builds_enabled? && commands.present? && manual? && skipped?
+ project.builds_enabled? && has_commands? &&
+ action? && manual?
+ end
+
+ def action?
+ self.when == 'manual'
+ end
+
+ def has_commands?
+ commands.present?
end
def play(current_user)
@@ -129,7 +131,7 @@ module Ci
end
def retryable?
- project.builds_enabled? && commands.present? &&
+ project.builds_enabled? && has_commands? &&
(success? || failed? || canceled?)
end
@@ -221,7 +223,8 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
- .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+ .where(source_branch: ref,
+ source_project: pipeline.project)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
@@ -229,14 +232,6 @@ module Ci
end
end
- def project_id
- gl_project_id
- end
-
- def project_name
- project.name
- end
-
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -257,7 +252,7 @@ module Ci
return unless regex
matches = text.scan(Regexp.new(regex)).last
- matches = matches.last if matches.kind_of?(Array)
+ matches = matches.last if matches.is_a?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
@@ -486,7 +481,7 @@ module Ci
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
- Time.now + ChronicDuration.parse(value)
+ ChronicDuration.parse(value)&.seconds&.from_now
end
end
@@ -519,10 +514,41 @@ module Ci
]
end
+ def steps
+ [Gitlab::Ci::Build::Step.from_commands(self),
+ Gitlab::Ci::Build::Step.from_after_script(self)].compact
+ end
+
+ def image
+ Gitlab::Ci::Build::Image.from_image(self)
+ end
+
+ def services
+ Gitlab::Ci::Build::Image.from_services(self)
+ end
+
+ def artifacts
+ [options[:artifacts]]
+ end
+
+ def cache
+ [options[:cache]]
+ end
+
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
+ def dependencies
+ depended_jobs = depends_on_builds
+
+ return depended_jobs unless options[:dependencies].present?
+
+ depended_jobs.select do |job|
+ options[:dependencies].include?(job.name)
+ end
+ end
+
private
def update_artifacts_size
@@ -542,13 +568,38 @@ module Ci
end
def unscoped_project
- @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ @unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
+ CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_JOB_ID', value: id.to_s, public: true },
+ { key: 'CI_JOB_NAME', value: name, public: true },
+ { key: 'CI_JOB_STAGE', value: stage, public: true },
+ { key: 'CI_JOB_TOKEN', value: token, public: false },
+ { key: 'CI_COMMIT_SHA', value: sha, public: true },
+ { key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
+ { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
+ { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
+ { key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
+ ]
+
+ variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
+ variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
+ variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
+ variables.concat(legacy_variables)
+ end
+
+ def legacy_variables
+ variables = [
{ key: 'CI_BUILD_ID', value: id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true },
@@ -556,14 +607,12 @@ module Ci
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
- { key: 'CI_BUILD_STAGE', value: stage, public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
+ { key: 'CI_BUILD_STAGE', value: stage, public: true }
]
- variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
- variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
- variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual?
+
+ variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
+ variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
+ variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
variables
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index dc4590a9923..f12be98c80c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,21 +5,22 @@ module Ci
include Importable
include AfterCommitQueue
- self.table_name = 'ci_commits'
-
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
- validates_presence_of :sha, unless: :importing?
- validates_presence_of :ref, unless: :importing?
- validates_presence_of :status, unless: :importing?
+ delegate :id, to: :project, prefix: true
+
+ validates :sha, presence: { unless: :importing? }
+ validates :ref, presence: { unless: :importing? }
+ validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
+ after_create :refresh_build_status_cache
state_machine :status, initial: :created do
event :enqueue do
@@ -47,6 +48,10 @@ module Ci
transition any - [:canceled] => :canceled
end
+ event :block do
+ transition any - [:manual] => :manual
+ end
+
# IMPORTANT
# Do not add any operations to this state_machine
# Create a separate worker for each new operation
@@ -93,8 +98,11 @@ module Ci
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
- relation = ref ? where(ref: ref) : self
- relation.where(id: max_id)
+ if ref
+ where(ref: ref, id: max_id.where(ref: ref))
+ else
+ where(id: max_id)
+ end
end
def self.latest_status(ref = nil)
@@ -105,6 +113,12 @@ module Ci
success.latest(ref).order(id: :desc).first
end
+ def self.latest_successful_for_refs(refs)
+ success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+ hash[pipeline.ref] ||= pipeline
+ end
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -135,7 +149,7 @@ module Ci
status_sql = statuses.latest.where('stage=sg.stage').status_sql
- warnings_sql = statuses.latest.select('COUNT(*) > 0')
+ warnings_sql = statuses.latest.select('COUNT(*)')
.where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg)
@@ -150,10 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
- def project_id
- project.id
- end
-
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
@@ -320,8 +330,10 @@ module Ci
when 'failed' then drop
when 'canceled' then cancel
when 'skipped' then skip
+ when 'manual' then block
end
end
+ refresh_build_status_cache
end
def predefined_variables
@@ -363,6 +375,10 @@ module Ci
.fabricate!
end
+ def refresh_build_status_cache
+ Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb
new file mode 100644
index 00000000000..048047d0e34
--- /dev/null
+++ b/app/models/ci/pipeline_status.rb
@@ -0,0 +1,86 @@
+# This class is not backed by a table in the main database.
+# It loads the latest Pipeline for the HEAD of a repository, and caches that
+# in Redis.
+module Ci
+ class PipelineStatus
+ attr_accessor :sha, :status, :project, :loaded
+
+ delegate :commit, to: :project
+
+ def self.load_for_project(project)
+ new(project).tap do |status|
+ status.load_status
+ end
+ end
+
+ def initialize(project, sha: nil, status: nil)
+ @project = project
+ @sha = sha
+ @status = status
+ end
+
+ def has_status?
+ loaded? && sha.present? && status.present?
+ end
+
+ def load_status
+ return if loaded?
+
+ if has_cache?
+ load_from_cache
+ else
+ load_from_commit
+ store_in_cache
+ end
+
+ self.loaded = true
+ end
+
+ def load_from_commit
+ return unless commit
+
+ self.sha = commit.sha
+ self.status = commit.status
+ end
+
+ # We only cache the status for the HEAD commit of a project
+ # This status is rendered in project lists
+ def store_in_cache_if_needed
+ return unless sha
+ return delete_from_cache unless commit
+ store_in_cache if commit.sha == self.sha
+ end
+
+ def load_from_cache
+ Gitlab::Redis.with do |redis|
+ self.sha, self.status = redis.hmget(cache_key, :sha, :status)
+ end
+ end
+
+ def store_in_cache
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key, { sha: sha, status: status })
+ end
+ end
+
+ def delete_from_cache
+ Gitlab::Redis.with do |redis|
+ redis.del(cache_key)
+ end
+ end
+
+ def has_cache?
+ Gitlab::Redis.with do |redis|
+ redis.exists(cache_key)
+ end
+ end
+
+ def loaded?
+ self.loaded
+ end
+
+ def cache_key
+ "projects/#{project.id}/build_status"
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 07a086b0aca..487ba61bc9c 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,12 +4,12 @@ module Ci
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago
- AVAILABLE_SCOPES = %w[specific shared active paused online]
- FORM_EDITABLE = %i[description tag_list active run_untagged locked]
+ AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy
- has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
@@ -24,7 +24,7 @@ module Ci
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
scope :assignable_for, ->(project) do
@@ -127,18 +127,15 @@ module Ci
def tick_runner_queue
SecureRandom.hex.tap do |new_update|
- Gitlab::Redis.with do |redis|
- redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
- end
+ ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
+ expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true)
end
end
def ensure_runner_queue_value
- Gitlab::Redis.with do |redis|
- value = SecureRandom.hex
- redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
- redis.get(runner_queue_key)
- end
+ new_value = SecureRandom.hex
+ ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value,
+ expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
end
def is_runner_queue_value_latest?(value)
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 1f9baeca5b1..5f01a0daae9 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :runner
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
- validates_uniqueness_of :runner_id, scope: :gl_project_id
+ validates :runner_id, uniqueness: { scope: :project_id }
end
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index ca74c91b062..e7d6b17d445 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -46,10 +46,10 @@ module Ci
end
def has_warnings?
- if @warnings.nil?
- statuses.latest.failed_but_allowed.any?
+ if @warnings.is_a?(Integer)
+ @warnings > 0
else
- @warnings
+ statuses.latest.failed_but_allowed.any?
end
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 62889fe80d8..cba1d81a861 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,11 +4,12 @@ module Ci
acts_as_paranoid
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
+ belongs_to :owner, class_name: "User"
+
has_many :trigger_requests, dependent: :destroy
- validates_presence_of :token
- validates_uniqueness_of :token
+ validates :token, presence: true, uniqueness: true
before_validation :set_default_values
@@ -25,7 +26,15 @@ module Ci
end
def short_token
- token[0...10]
+ token[0...4]
+ end
+
+ def legacy?
+ self.owner_id.blank?
+ end
+
+ def can_access_project?
+ self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end
end
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5d..6c6586110c5 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
validates :key,
presence: true,
- uniqueness: { scope: :gl_project_id },
+ uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 46f06733da1..ce92cc369ad 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,12 +22,12 @@ class Commit
DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
- COMMIT_SHA_PATTERN = '\h{7,40}'
+ COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
class << self
def decorate(commits, project)
commits.map do |commit|
- if commit.kind_of?(Commit)
+ if commit.is_a?(Commit)
commit
else
self.new(commit, project)
@@ -105,7 +105,7 @@ class Commit
end
def diff_line_count
- @diff_line_count ||= Commit::diff_line_count(raw_diffs)
+ @diff_line_count ||= Commit.diff_line_count(raw_diffs)
@diff_line_count
end
@@ -122,11 +122,12 @@ class Commit
def full_title
return @full_title if @full_title
- if safe_message.blank?
- @full_title = no_commit_message
- else
- @full_title = safe_message.split("\n", 2).first
- end
+ @full_title =
+ if safe_message.blank?
+ no_commit_message
+ else
+ safe_message.split("\n", 2).first
+ end
end
# Returns the commits description
@@ -230,6 +231,10 @@ class Commit
project.pipelines.where(sha: sha)
end
+ def latest_pipeline
+ pipelines.last
+ end
+
def status(ref = nil)
@statuses ||= {}
@@ -316,7 +321,14 @@ class Commit
end
def raw_diffs(*args)
- raw.diffs(*args)
+ use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+ if use_gitaly && !deltas_only
+ Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ else
+ raw.diffs(*args)
+ end
end
def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 99a6326309d..8c71267da65 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,15 +5,16 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
delegate :commit, to: :pipeline
+ delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
- validates_presence_of :name
+ validates :name, presence: true
alias_attribute :author, :user
@@ -28,9 +29,11 @@ class CommitStatus < ActiveRecord::Base
end
scope :exclude_ignored, -> do
- # We want to ignore failed_but_allowed jobs
+ # We want to ignore failed but allowed to fail jobs.
+ #
+ # TODO, we also skip ignored optional manual actions.
where("allow_failure = ? OR status IN (?)",
- false, all_state_names - [:failed, :canceled])
+ false, all_state_names - [:failed, :canceled, :manual])
end
scope :retried, -> { where.not(id: latest) }
@@ -41,11 +44,11 @@ class CommitStatus < ActiveRecord::Base
state_machine :status do
event :enqueue do
- transition [:created, :skipped] => :pending
+ transition [:created, :skipped, :manual] => :pending
end
event :process do
- transition skipped: :created
+ transition [:skipped, :manual] => :created
end
event :run do
@@ -65,7 +68,7 @@ class CommitStatus < ActiveRecord::Base
end
event :cancel do
- transition [:created, :pending, :running] => :canceled
+ transition [:created, :pending, :running, :manual] => :canceled
end
before_transition created: [:pending, :running] do |commit_status|
@@ -85,7 +88,7 @@ class CommitStatus < ActiveRecord::Base
commit_status.run_after_commit do
pipeline.try do |pipeline|
- if complete?
+ if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
@@ -102,8 +105,6 @@ class CommitStatus < ActiveRecord::Base
end
end
- delegate :sha, :short_sha, to: :pipeline
-
def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
@@ -132,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
false
end
+ # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+ # and prior.
+ def gl_project_id
+ 'dummy'
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Factory
.new(self, current_user)
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 073ac4c1b65..a7fd0a15f0f 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -101,6 +101,6 @@ module Awardable
private
def normalize_name(name)
- Gitlab::AwardEmoji.normalize_emoji_name(name)
+ Gitlab::Emoji.normalize_emoji_name(name)
end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index a600f9c14c5..8ea95beed79 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,14 +11,15 @@ module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
- extend Forwardable
-
def initialize
@data = {}
end
- def_delegators :@data, :[], :[]=
- def_delegator :@data, :keys, :markdown_fields
+ delegate :[], :[]=, to: :@data
+
+ def markdown_fields
+ @data.keys
+ end
def html_field(markdown_field)
"#{markdown_field}_html"
@@ -45,7 +46,7 @@ module CacheMarkdownField
Project
Release
Snippet
- ]
+ ].freeze
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index fe0cea8465f..034e9f40ff0 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -13,11 +13,12 @@ module CaseSensitivity
params.each do |key, value|
column = ActiveRecord::Base.connection.quote_table_name(key)
- if cast_lower
- condition = "LOWER(#{column}) = LOWER(:value)"
- else
- condition = "#{column} = :value"
- end
+ condition =
+ if cast_lower
+ "LOWER(#{column}) = LOWER(:value)"
+ else
+ "#{column} = :value"
+ end
criteria = criteria.where(condition, value: value)
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 431c0354969..0a1a65da05a 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,23 +1,22 @@
module HasStatus
extend ActiveSupport::Concern
- DEFAULT_STATUS = 'created'
- AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
- STARTED_STATUSES = %w[running success failed skipped]
- ACTIVE_STATUSES = %w[pending running]
- COMPLETED_STATUSES = %w[success failed canceled skipped]
- ORDERED_STATUSES = %w[failed pending running canceled success skipped]
+ DEFAULT_STATUS = 'created'.freeze
+ BLOCKED_STATUS = 'manual'.freeze
+ AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze
+ STARTED_STATUSES = %w[running success failed skipped manual].freeze
+ ACTIVE_STATUSES = %w[pending running].freeze
+ COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
+ ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
class_methods do
def status_sql
- scope = if respond_to?(:exclude_ignored)
- exclude_ignored
- else
- all
- end
+ scope = respond_to?(:exclude_ignored) ? exclude_ignored : all
+
builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
+ manual = scope.manual.select('count(*)').to_sql
pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql
@@ -30,7 +29,9 @@ module HasStatus
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
- WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
+ WHEN (#{running})+(#{pending})>0 THEN 'running'
+ WHEN (#{manual})>0 THEN 'manual'
+ WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
end
@@ -63,6 +64,7 @@ module HasStatus
state :success, value: 'success'
state :canceled, value: 'canceled'
state :skipped, value: 'skipped'
+ state :manual, value: 'manual'
end
scope :created, -> { where(status: 'created') }
@@ -73,12 +75,13 @@ module HasStatus
scope :failed, -> { where(status: 'failed') }
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
+ scope :manual, -> { where(status: 'manual') }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
scope :cancelable, -> do
- where(status: [:running, :pending, :created])
+ where(status: [:running, :pending, :created, :manual])
end
end
@@ -94,6 +97,10 @@ module HasStatus
COMPLETED_STATUSES.include?(status)
end
+ def blocked?
+ BLOCKED_STATUS == status
+ end
+
private
def calculate_duration
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5f53c48fc88..e7bd20b322a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -16,9 +16,9 @@ module Issuable
include TimeTrackable
# This object is used to gather issuable meta data for displaying
- # upvotes, downvotes and notes count for issues and merge requests
+ # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
- IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count)
included do
cache_markdown_field :title, pipeline: :single_line
@@ -46,12 +46,26 @@ module Issuable
has_one :metrics
+ delegate :name,
+ :email,
+ :public_email,
+ to: :author,
+ prefix: true
+
+ delegate :name,
+ :email,
+ :public_email,
+ to: :assignee,
+ allow_nil: true,
+ prefix: true
+
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :recent, -> { reorder(id: :desc) }
+ scope :order_position_asc, -> { reorder(position: :asc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -68,21 +82,10 @@ module Issuable
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
- scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) }
+ scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
- delegate :name,
- :email,
- to: :author,
- prefix: true
-
- delegate :name,
- :email,
- to: :assignee,
- allow_nil: true,
- prefix: true
-
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
@@ -143,7 +146,9 @@ module Issuable
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
- when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
+ when 'position_asc' then order_position_asc
else
order_by(method)
end
@@ -152,7 +157,28 @@ module Issuable
sorted.order(id: :desc)
end
- def order_labels_priority(excluded_labels: [])
+ def order_due_date_and_labels_priority(excluded_labels: [])
+ # The order_ methods also modify the query in other ways:
+ #
+ # - For milestones, we add a JOIN.
+ # - For label priority, we change the SELECT, and add a GROUP BY.#
+ #
+ # After doing those, we need to reorder to the order we want. The existing
+ # ORDER BYs won't work because:
+ #
+ # 1. We need milestone due date first.
+ # 2. We can't ORDER BY a column that isn't in the GROUP BY and doesn't
+ # have an aggregate function applied, so we do a useless MIN() instead.
+ #
+ milestones_due_date = 'MIN(milestones.due_date)'
+
+ order_milestone_due_asc.
+ order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]).
+ reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'),
+ Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ end
+
+ def order_labels_priority(excluded_labels: [], extra_select_columns: [])
params = {
target_type: name,
target_column: "#{table_name}.id",
@@ -162,7 +188,12 @@ module Issuable
highest_priority = highest_label_priority(params).to_sql
- select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ select_columns = [
+ "#{table_name}.*",
+ "(#{highest_priority}) AS highest_priority"
+ ] + extra_select_columns
+
+ select(select_columns.join(', ')).
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -182,7 +213,7 @@ module Issuable
def grouping_columns(sort)
grouping_columns = [arel_table[:id]]
- if ["milestone_due_desc", "milestone_due_asc"].include?(sort)
+ if %w(milestone_due_desc milestone_due_asc).include?(sort)
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
@@ -232,10 +263,11 @@ module Issuable
user: user.hook_attrs,
project: project.hook_attrs,
object_attributes: hook_attrs,
+ labels: labels.map(&:hook_attrs),
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
- hook_data.merge!(assignee: assignee.hook_attrs) if assignee
+ hook_data[:assignee] = assignee.hook_attrs if assignee
hook_data
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
index e1f868a299b..713246039c1 100644
--- a/app/models/concerns/reactive_service.rb
+++ b/app/models/concerns/reactive_service.rb
@@ -5,6 +5,6 @@ module ReactiveService
include ReactiveCaching
# Default cache key: class name + project_id
- self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
end
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
new file mode 100644
index 00000000000..f1d8532a6d6
--- /dev/null
+++ b/app/models/concerns/relative_positioning.rb
@@ -0,0 +1,139 @@
+module RelativePositioning
+ extend ActiveSupport::Concern
+
+ MIN_POSITION = 0
+ START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
+ MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+ IDEAL_DISTANCE = 500
+
+ included do
+ after_save :save_positionable_neighbours
+ end
+
+ def max_relative_position
+ self.class.in_projects(project.id).maximum(:relative_position)
+ end
+
+ def prev_relative_position
+ prev_pos = nil
+
+ if self.relative_position
+ prev_pos = self.class.
+ in_projects(project.id).
+ where('relative_position < ?', self.relative_position).
+ maximum(:relative_position)
+ end
+
+ prev_pos
+ end
+
+ def next_relative_position
+ next_pos = nil
+
+ if self.relative_position
+ next_pos = self.class.
+ in_projects(project.id).
+ where('relative_position > ?', self.relative_position).
+ minimum(:relative_position)
+ end
+
+ next_pos
+ end
+
+ def move_between(before, after)
+ return move_after(before) unless after
+ return move_before(after) unless before
+
+ # If there is no place to insert an issue we need to create one by moving the before issue closer
+ # to its predecessor. This process will recursively move all the predecessors until we have a place
+ if (after.relative_position - before.relative_position) < 2
+ before.move_before
+ @positionable_neighbours = [before]
+ end
+
+ self.relative_position = position_between(before.relative_position, after.relative_position)
+ end
+
+ def move_after(before = self)
+ pos_before = before.relative_position
+ pos_after = before.next_relative_position
+
+ if before.shift_after?
+ issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
+ issue_to_move.move_after
+ @positionable_neighbours = [issue_to_move]
+
+ pos_after = issue_to_move.relative_position
+ end
+
+ self.relative_position = position_between(pos_before, pos_after)
+ end
+
+ def move_before(after = self)
+ pos_after = after.relative_position
+ pos_before = after.prev_relative_position
+
+ if after.shift_before?
+ issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
+ issue_to_move.move_before
+ @positionable_neighbours = [issue_to_move]
+
+ pos_before = issue_to_move.relative_position
+ end
+
+ self.relative_position = position_between(pos_before, pos_after)
+ end
+
+ def move_to_end
+ self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
+ end
+
+ # Indicates if there is an issue that should be shifted to free the place
+ def shift_after?
+ next_pos = next_relative_position
+ next_pos && (next_pos - relative_position) == 1
+ end
+
+ # Indicates if there is an issue that should be shifted to free the place
+ def shift_before?
+ prev_pos = prev_relative_position
+ prev_pos && (relative_position - prev_pos) == 1
+ end
+
+ private
+
+ # This method takes two integer values (positions) and
+ # calculates the position between them. The range is huge as
+ # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
+ # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
+ def position_between(pos_before, pos_after)
+ pos_before ||= MIN_POSITION
+ pos_after ||= MAX_POSITION
+
+ pos_before, pos_after = [pos_before, pos_after].sort
+
+ halfway = (pos_after + pos_before) / 2
+ distance_to_halfway = pos_after - halfway
+
+ if distance_to_halfway < IDEAL_DISTANCE
+ halfway
+ else
+ if pos_before == MIN_POSITION
+ pos_after - IDEAL_DISTANCE
+ elsif pos_after == MAX_POSITION
+ pos_before + IDEAL_DISTANCE
+ else
+ halfway
+ end
+ end
+ end
+
+ def save_positionable_neighbours
+ return unless @positionable_neighbours
+
+ status = @positionable_neighbours.all?(&:save)
+ @positionable_neighbours = nil
+
+ status
+ end
+end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3..529fb5ce988 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
paths.each do |path|
path = connection.quote(path)
- where = "(routes.path = #{path})"
- if cast_lower
- where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
- end
+ where =
+ if cast_lower
+ "(LOWER(routes.path) = LOWER(#{path}))"
+ else
+ "(routes.path = #{path})"
+ end
wheres << where
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7edb0acd56c..b9a2d812edd 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -46,11 +46,12 @@ module Sortable
where("label_links.target_id = #{target_column}").
reorder(nil)
- if target_type_column
- query = query.where("label_links.target_type = #{target_type_column}")
- else
- query = query.where(label_links: { target_type: target_type })
- end
+ query =
+ if target_type_column
+ query.where("label_links.target_type = #{target_type_column}")
+ else
+ query.where(label_links: { target_type: target_type })
+ end
query = query.where.not(title: excluded_labels) if excluded_labels.present?
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
new file mode 100644
index 00000000000..a7fe5951b6e
--- /dev/null
+++ b/app/models/concerns/uniquify.rb
@@ -0,0 +1,30 @@
+class Uniquify
+ # Return a version of the given 'base' string that is unique
+ # by appending a counter to it. Uniqueness is determined by
+ # repeated calls to the passed block.
+ #
+ # If `base` is a function/proc, we expect that calling it with a
+ # candidate counter returns a string to test/return.
+ def string(base)
+ @base = base
+ @counter = nil
+
+ increment_counter! while yield(base_string)
+ base_string
+ end
+
+ private
+
+ def base_string
+ if @base.respond_to?(:call)
+ @base.call(@counter)
+ else
+ "#{@base}#{@counter}"
+ end
+ end
+
+ def increment_counter!
+ @counter ||= 0
+ @counter += 1
+ end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 559b3075905..895a91139c9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -8,7 +8,7 @@ class DiffNote < Note
validates :position, presence: true
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
- validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+ validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) }
validates :resolved_by, presence: true, if: :resolved?
validate :positions_complete
validate :verify_supported
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 1a21b5e52b5..bf33010fd21 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base
project.deployment_service.terminals(self) if has_terminals?
end
+ def has_metrics?
+ project.monitoring_service.present? && available? && last_deployment.present?
+ end
+
+ def metrics
+ project.monitoring_service.metrics(self) if has_metrics?
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/event.rb b/app/models/event.rb
index e5027df3f8a..5c34844b5d3 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
- delegate :name, :email, to: :author, prefix: true, allow_nil: true
+ delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do
- where(project_id: projects).recent
+ where(project_id: projects.pluck(:id)).recent
end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
Event::PUSHED,
- ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED],
+ %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
"Note", Event::COMMENTED)
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 26712c19b5a..e63f89a9f85 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -24,6 +24,11 @@ class ExternalIssue
def ==(other)
other.is_a?(self.class) && (to_s == other.to_s)
end
+ alias_method :eql?, :==
+
+ def hash
+ [self.class, to_s].hash
+ end
def project
@project
@@ -43,7 +48,7 @@ class ExternalIssue
end
def reference_link_text(from_project = nil)
- return "##{id}" if /^\d+$/.match(id)
+ return "##{id}" if id =~ /^\d+$/
id
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index b991d78e27f..0afbca2cb32 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -28,6 +28,28 @@ class GlobalMilestone
new(title, child_milestones)
end
+ def self.states_count(projects)
+ relation = MilestonesFinder.new.execute(projects, state: 'all')
+ milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
+
+ opened = count_by_state(milestones_by_state_and_title, 'active')
+ closed = count_by_state(milestones_by_state_and_title, 'closed')
+ all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
+
+ {
+ opened: opened,
+ closed: closed,
+ all: all
+ }
+ end
+
+ def self.count_by_state(milestones_by_state_and_title, state)
+ milestones_by_state_and_title.count do |(milestone_state, _), _|
+ milestone_state == state
+ end
+ end
+ private_class_method :count_by_state
+
def initialize(title, milestones)
@title = title
@name = title
diff --git a/app/models/group.rb b/app/models/group.rb
index 240a17f1dc1..bd0ecae3da4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -28,6 +28,7 @@ class Group < Namespace
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
+ has_many :uploads, as: :model, dependent: :destroy
after_create :post_create_hook
after_destroy :post_destroy_hook
@@ -93,7 +94,7 @@ class Group < Namespace
end
def visibility_level_field
- visibility_level
+ :visibility_level
end
def visibility_level_allowed_by_projects
@@ -212,4 +213,14 @@ class Group < Namespace
def users_with_parents
User.where(id: members_with_parents.select(:user_id))
end
+
+ def mattermost_team_params
+ max_length = 59
+
+ {
+ name: path[0..max_length],
+ display_name: name[0..max_length],
+ type: public? ? 'O' : 'I' # Open vs Invite-only
+ }
+ end
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
index 01285ca1264..df287c277a7 100644
--- a/app/models/guest.rb
+++ b/app/models/guest.rb
@@ -1,6 +1,6 @@
class Guest
class << self
- def can?(action, subject)
+ def can?(action, subject = :global)
Ability.allowed?(nil, action, subject)
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d8826b65fcc..602eed86d9e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
include Sortable
include Spammable
include FasterCacheKeys
+ include RelativePositioning
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -15,8 +16,6 @@ class Issue < ActiveRecord::Base
DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
- ActsAsTaggableOn.strict_case_match = true
-
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
@@ -56,10 +55,24 @@ class Issue < ActiveRecord::Base
state :opened
state :reopened
state :closed
+
+ before_transition any => :closed do |issue|
+ issue.closed_at = Time.zone.now
+ end
+
+ before_transition closed: any do |issue|
+ issue.closed_at = nil
+ end
end
def hook_attrs
- attributes
+ attrs = {
+ total_time_spent: total_time_spent,
+ human_total_time_spent: human_total_time_spent,
+ human_time_estimate: human_time_estimate
+ }
+
+ attributes.merge!(attrs)
end
def self.reference_prefix
@@ -97,6 +110,13 @@ class Issue < ActiveRecord::Base
end
end
+ def self.order_by_position_and_priority
+ order_labels_priority.
+ reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
+ Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
+ "id DESC")
+ end
+
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
diff --git a/app/models/label.rb b/app/models/label.rb
index 5b6b9a7a736..568fa6d44f5 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -11,7 +11,7 @@ class Label < ActiveRecord::Base
cache_markdown_field :description, pipeline: :single_line
- DEFAULT_COLOR = '#428BCA'
+ DEFAULT_COLOR = '#428BCA'.freeze
default_value_for :color, DEFAULT_COLOR
@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base
end
end
+ def hook_attrs
+ attributes
+ end
+
private
def issues_count(user, params = {})
diff --git a/app/models/member.rb b/app/models/member.rb
index d07f270b757..0545bd4eedf 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -10,6 +10,8 @@ class Member < ActiveRecord::Base
belongs_to :user
belongs_to :source, polymorphic: true
+ delegate :name, :username, :email, to: :user, prefix: true
+
validates :user, presence: true, unless: :invite?
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
@@ -73,8 +75,6 @@ class Member < ActiveRecord::Base
after_destroy :post_destroy_hook, unless: :pending?
after_commit :refresh_member_authorized_projects
- delegate :name, :username, :email, to: :user, prefix: true
-
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 204f34f0269..446f9f8f8a7 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,11 +1,11 @@
class GroupMember < Member
- SOURCE_TYPE = 'Namespace'
+ SOURCE_TYPE = 'Namespace'.freeze
belongs_to :group, foreign_key: 'source_id'
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
- validates_format_of :source_type, with: /\ANamespace\z/
+ validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
def self.access_level_roles
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 008fff0857c..912820b51ac 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -1,5 +1,5 @@
class ProjectMember < Member
- SOURCE_TYPE = 'Project'
+ SOURCE_TYPE = 'Project'.freeze
include Gitlab::ShellAdapter
@@ -7,7 +7,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
- validates_format_of :source_type, with: /\AProject\z/
+ validates :source_type, format: { with: /\AProject\z/ }
validates :access_level, inclusion: { in: Gitlab::Access.values }
default_scope { where(source_type: SOURCE_TYPE) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 204d2b153ad..cef8ad76b07 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
+ belongs_to :project, foreign_key: :target_project_id
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy
@@ -91,17 +92,13 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
-
- after_transition unchecked: :cannot_be_merged do |merge_request, transition|
- TodoService.new.merge_request_became_unmergeable(merge_request)
- end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
- validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing?
+ validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
@@ -203,7 +200,11 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- opts = diff_options || {}
+ # The `#diffs` method ends up at an instance of a class inheriting from
+ # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
+ # here too, to get the same diff size without performing highlighting.
+ #
+ opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
raw_diffs(opts).size
end
@@ -436,7 +437,7 @@ class MergeRequest < ActiveRecord::Base
true
end
- def can_cancel_merge_when_build_succeeds?(current_user)
+ def can_cancel_merge_when_pipeline_succeeds?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
@@ -523,11 +524,14 @@ class MergeRequest < ActiveRecord::Base
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
- work_in_progress: work_in_progress?
+ work_in_progress: work_in_progress?,
+ total_time_spent: total_time_spent,
+ human_total_time_spent: human_total_time_spent,
+ human_time_estimate: human_time_estimate
}
if diff_head_commit
- attrs.merge!(last_commit: diff_head_commit.hook_attrs)
+ attrs[:last_commit] = diff_head_commit.hook_attrs
end
attributes.merge!(attrs)
@@ -537,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project
end
- def project
- target_project
- end
-
# If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
@@ -644,10 +644,10 @@ class MergeRequest < ActiveRecord::Base
message.join("\n\n")
end
- def reset_merge_when_build_succeeds
- return unless merge_when_build_succeeds?
+ def reset_merge_when_pipeline_succeeds
+ return unless merge_when_pipeline_succeeds?
- self.merge_when_build_succeeds = false
+ self.merge_when_pipeline_succeeds = false
self.merge_user = nil
if merge_params
merge_params.delete('should_remove_source_branch')
@@ -684,7 +684,10 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- source_project.try(:ci_service) && commits.any?
+ has_ci_integration = source_project.try(:ci_service)
+ uses_gitlab_ci = all_pipelines.any?
+
+ (has_ci_integration || uses_gitlab_ci) && commits.any?
end
def branch_missing?
@@ -706,7 +709,7 @@ class MergeRequest < ActiveRecord::Base
end
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_build_succeeds?
+ return true unless project.only_allow_merge_if_pipeline_succeeds?
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 70bad2a4396..baee00b8fcd 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base
COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
- VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+ VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
belongs_to :merge_request
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
index ab597c37947..daafb137be4 100644
--- a/app/models/merge_requests_closing_issues.rb
+++ b/app/models/merge_requests_closing_issues.rb
@@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
validates :issue_id, presence: true
+
+ class << self
+ def count_for_collection(ids)
+ group(:issue_id).
+ where(issue_id: ids).
+ pluck('issue_id', 'COUNT(*) as count')
+ end
+ end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 7331000a9f2..c0deb59ec4c 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+ Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include InternalId
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a803be2e780..4ae9d0122f2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -20,6 +20,7 @@ class Namespace < ActiveRecord::Base
belongs_to :parent, class_name: "Namespace"
has_many :children, class_name: "Namespace", foreign_key: :parent_id
+ has_one :chat_team, dependent: :destroy
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
@@ -98,14 +99,8 @@ class Namespace < ActiveRecord::Base
# Work around that by setting their username to "blank", followed by a counter.
path = "blank" if path.blank?
- counter = 0
- base = path
- while Namespace.find_by_path_or_name(path)
- counter += 1
- path = "#{base}#{counter}"
- end
-
- path
+ uniquify = Uniquify.new
+ uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index b524ca50ee8..0bbc9451ffd 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -188,11 +188,12 @@ module Network
end
# and mark it as reserved
- if parent_time.nil?
- min_time = leaves.first.time
- else
- min_time = parent_time + 1
- end
+ min_time =
+ if parent_time.nil?
+ leaves.first.time
+ else
+ parent_time + 1
+ end
max_time = leaves.last.time
leaves.last.parents(@map).each do |parent|
diff --git a/app/models/note.rb b/app/models/note.rb
index 029fe667a45..e22e96aec6f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -72,7 +72,7 @@ class Note < ActiveRecord::Base
scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
- scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
+ scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
scope :with_associations, -> do
@@ -85,6 +85,7 @@ class Note < ActiveRecord::Base
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
after_save :keep_around_commit, unless: :for_personal_snippet?
+ after_save :expire_etag_cache
class << self
def model_name
@@ -231,10 +232,6 @@ class Note < ActiveRecord::Base
note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
end
- def award_emoji_name
- note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
- end
-
def to_ability_name
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
end
@@ -276,4 +273,16 @@ class Note < ActiveRecord::Base
self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
end
end
+
+ def expire_etag_cache
+ return unless for_issue?
+
+ key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path(
+ noteable.project.namespace,
+ noteable.project,
+ target_type: noteable_type.underscore,
+ target_id: noteable.id
+ )
+ Gitlab::EtagCaching::Store.new.touch(key)
+ end
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 58f6214bea7..52577bd52ea 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -35,11 +35,11 @@ class NotificationSetting < ActiveRecord::Base
:merge_merge_request,
:failed_pipeline,
:success_pipeline
- ]
+ ].freeze
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
- ]
+ ].freeze
store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb
new file mode 100644
index 00000000000..3a997406565
--- /dev/null
+++ b/app/models/oauth_access_grant.rb
@@ -0,0 +1,4 @@
+class OauthAccessGrant < Doorkeeper::AccessGrant
+ belongs_to :resource_owner, class_name: 'User'
+ belongs_to :application, class_name: 'Doorkeeper::Application'
+end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 116fb71ac08..b85f5dbaf2e 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -1,4 +1,4 @@
-class OauthAccessToken < ActiveRecord::Base
+class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 0b9ebf1ffe2..f2f2fc1e32a 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -2,7 +2,7 @@ class PagesDomain < ActiveRecord::Base
belongs_to :project
validates :domain, hostname: true
- validates_uniqueness_of :domain, case_sensitive: false
+ validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 10a34c42fd8..e8b000ddad6 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -1,4 +1,5 @@
class PersonalAccessToken < ActiveRecord::Base
+ include Expirable
include TokenAuthenticatable
add_authentication_token_field :token
@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user
- scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+ before_save :ensure_token
+
+ scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+ scope :with_impersonation, -> { where(impersonation: true) }
+ scope :without_impersonation, -> { where(impersonation: false) }
- def self.generate(params)
- personal_access_token = self.new(params)
- personal_access_token.ensure_token
- personal_access_token
- end
+ validates :scopes, presence: true
+ validate :validate_api_scopes
def revoke!
self.revoked = true
self.save
end
+
+ def active?
+ !revoked? && !expired?
+ end
+
+ protected
+
+ def validate_api_scopes
+ unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
+ errors.add :scopes, "can only contain API scopes"
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index d4f5584f53d..928965643a0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,10 +19,10 @@ class Project < ActiveRecord::Base
extend Gitlab::ConfigHelper
- class BoardLimitExceeded < StandardError; end
+ BoardLimitExceeded = Class.new(StandardError)
NUMBER_OF_PERMITTED_BOARDS = 1
- UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
cache_markdown_field :description, pipeline: :description
@@ -70,8 +70,7 @@ class Project < ActiveRecord::Base
after_validation :check_pending_delete
- ActsAsTaggableOn.strict_case_match = true
- acts_as_taggable_on :tags
+ acts_as_taggable
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
@@ -90,7 +89,6 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
- has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
@@ -114,6 +112,8 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
+ has_one :prometheus_service, dependent: :destroy, inverse_of: :project
+ has_one :mock_ci_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
@@ -159,13 +159,13 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_images, dependent: :destroy
- has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
- has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
- has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+ has_many :commit_statuses, dependent: :destroy
+ has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+ has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
@@ -173,9 +173,11 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
+ delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
+ delegate :empty_repo?, to: :repository
# Validations
validates :creator, presence: true, on: :create
@@ -192,9 +194,10 @@ class Project < ActiveRecord::Base
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :namespace, presence: true
- validates_uniqueness_of :name, scope: :namespace_id
- validates_uniqueness_of :path, scope: :namespace_id
+ validates :name, uniqueness: { scope: :namespace_id }
+ validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
+ validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
@@ -211,6 +214,7 @@ class Project < ActiveRecord::Base
before_save :ensure_runners_token
mount_uploader :avatar, AvatarUploader
+ has_many :uploads, as: :model, dependent: :destroy
# Scopes
default_scope { where(pending_delete: false) }
@@ -334,7 +338,7 @@ class Project < ActiveRecord::Base
end
def search_by_visibility(level)
- where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase))
+ where(visibility_level: Gitlab::VisibilityLevel.string_options[level])
end
def search_by_title(query)
@@ -359,7 +363,7 @@ class Project < ActiveRecord::Base
end
def reference_pattern
- name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
+ name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{
((?<namespace>#{name_pattern})\/)?
@@ -390,7 +394,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]
+ Gitlab.config.repositories.storages[repository_storage]['path']
end
def team
@@ -452,13 +456,14 @@ class Project < ActiveRecord::Base
end
def add_import_job
- if forked?
- job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
- forked_from_project.path_with_namespace,
- self.namespace.full_path)
- else
- job_id = RepositoryImportWorker.perform_async(self.id)
- end
+ job_id =
+ if forked?
+ RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace,
+ self.namespace.full_path)
+ else
+ RepositoryImportWorker.perform_async(self.id)
+ end
if job_id
Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
@@ -766,6 +771,14 @@ class Project < ActiveRecord::Base
@deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
end
+ def monitoring_services
+ services.where(category: :monitoring)
+ end
+
+ def monitoring_service
+ @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
+ end
+
def jira_tracker?
issues_tracker.to_param == 'jira'
end
@@ -836,10 +849,6 @@ class Project < ActiveRecord::Base
false
end
- def empty_repo?
- repository.empty_repo?
- end
-
def repo
repository.raw
end
@@ -848,10 +857,6 @@ class Project < ActiveRecord::Base
gitlab_shell.url_to_repo(path_with_namespace)
end
- def namespace_dir
- namespace.try(:path) || ''
- end
-
def repo_exists?
@repo_exists ||= repository.exists?
rescue
@@ -874,8 +879,10 @@ class Project < ActiveRecord::Base
url_to_repo
end
- def http_url_to_repo
- "#{web_url}.git"
+ def http_url_to_repo(user = nil)
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+ Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
end
# Check if current branch name is marked as protected in the system
@@ -900,8 +907,8 @@ class Project < ActiveRecord::Base
def rename_repo
path_was = previous_changes['path'].first
- old_path_with_namespace = File.join(namespace_dir, path_was)
- new_path_with_namespace = File.join(namespace_dir, path)
+ old_path_with_namespace = File.join(namespace.full_path, path_was)
+ new_path_with_namespace = File.join(namespace.full_path, path)
Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
@@ -1002,7 +1009,7 @@ class Project < ActiveRecord::Base
end
def visibility_level_field
- visibility_level
+ :visibility_level
end
def archive!
@@ -1027,10 +1034,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project
end
- def forks_count
- forks.count
- end
-
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
@@ -1201,6 +1204,10 @@ class Project < ActiveRecord::Base
end
end
+ def pipeline_status
+ @pipeline_status ||= Ci::PipelineStatus.load_for_project(self)
+ end
+
def mark_import_as_failed(error_message)
original_errors = errors.dup
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 03194fc2141..e3ef4919b28 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -18,7 +18,7 @@ class ProjectFeature < ActiveRecord::Base
PRIVATE = 10
ENABLED = 20
- FEATURES = %i(issues merge_requests wiki snippets builds repository)
+ FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
class << self
def access_level_attribute(feature)
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 5cb6b0c527d..ac1e9ab2b0b 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base
private
def different_group
- if self.group && self.project && self.project.group == self.group
- errors.add(:base, "Project cannot be shared with the project it is in.")
+ return unless self.group && self.project
+
+ project_group = self.project.group
+ return unless project_group
+
+ group_ids = project_group.ancestors.map(&:id).push(project_group.id)
+
+ if group_ids.include?(self.group.id)
+ errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.")
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 0956c4a4ede..5fb95050b83 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,7 @@ require "addressable/uri"
class BuildkiteService < CiService
include ReactiveService
- ENDPOINT = "https://buildkite.com"
+ ENDPOINT = "https://buildkite.com".freeze
prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e37189..0c526b53d72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
class BuildsEmailService < Service
- prop_accessor :recipients
- boolean_accessor :add_pusher
- boolean_accessor :notify_only_broken_builds
- validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- end
- end
-
- def title
- 'Builds emails'
- end
-
- def description
- 'Email the builds status to a list of recipients.'
- end
-
def self.to_param
'builds_email'
end
def self.supported_events
- %w(build)
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return unless should_build_be_notified?(push_data)
-
- recipients = all_recipients(push_data)
-
- if recipients.any?
- BuildEmailWorker.perform_async(
- push_data[:build_id],
- recipients,
- push_data
- )
- end
- end
-
- def can_test?
- project.builds.any?
- end
-
- def disabled_title
- "Please setup a build on your repository."
- end
-
- def test_data(project = nil, user = nil)
- Gitlab::DataBuilder::Build.build(project.builds.last)
- end
-
- def fields
- [
- { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
- { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- ]
- end
-
- def test(data)
- begin
- # bypass build status verification when testing
- data[:build_status] = "failed"
- data[:build_allow_failure] = false
-
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
- def should_build_be_notified?(data)
- case data[:build_status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- !allow_failure?(data)
- else
- false
- end
- end
-
- def allow_failure?(data)
- data[:build_allow_failure] == true
- end
-
- def all_recipients(data)
- all_recipients = []
-
- unless recipients.blank?
- all_recipients += recipients.split(',').compact.reject(&:blank?)
- end
-
- if add_pusher? && data[:user][:email]
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ %w[]
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c4..00000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
- class BuildMessage < BaseMessage
- attr_reader :sha
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :user_name
- attr_reader :user_url
- attr_reader :duration
- attr_reader :stage
- attr_reader :build_id
- attr_reader :build_name
-
- def initialize(params)
- @sha = params[:sha]
- @ref_type = params[:tag] ? 'tag' : 'branch'
- @ref = params[:ref]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @status = params[:commit][:status]
- @user_name = params[:commit][:author_name]
- @user_url = params[:commit][:author_url]
- @duration = params[:commit][:duration]
- @stage = params[:build_stage]
- @build_name = params[:build_name]
- @build_id = params[:build_id]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def build_url
- "#{project_url}/builds/#{build_id}"
- end
-
- def build_link
- link(build_name, build_url)
- end
-
- def user_link
- link(user_name, user_url)
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- link(ref, branch_url)
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def commit_url
- "#{project_url}/commit/#{sha}/builds"
- end
-
- def commit_link
- link(Commit.truncate_sha(sha), commit_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index b96aca47e65..791e5b0cec7 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -51,7 +51,8 @@ module ChatMessage
title: issue_title,
title_link: issue_url,
text: format(description),
- color: "#C95823" }]
+ color: "#C95823"
+ }]
end
def project_link
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425f..200be99f36b 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+ boolean_accessor :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
@@ -16,7 +16,6 @@ class ChatNotificationService < Service
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
@@ -27,7 +26,7 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
+ pipeline wiki_page]
end
def execute(data)
@@ -89,8 +88,6 @@ class ChatNotificationService < Service
ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
- when "build"
- ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
@@ -125,17 +122,6 @@ class ChatNotificationService < Service
data[:object_attributes][:action] == 'update'
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
- end
-
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 1ad9efac196..2717c240f05 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -39,7 +39,7 @@ class DroneCiService < CiService
def commit_status_path(sha, ref)
url = [drone_url,
"gitlab/#{project.full_path}/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
+ "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s
end
@@ -74,7 +74,7 @@ class DroneCiService < CiService
def build_page(sha, ref)
url = [drone_url,
"gitlab/#{project.full_path}/redirect/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}"]
+ "?branch=#{URI.encode(ref.to_s)}"]
URI.join(*url).to_s
end
@@ -114,7 +114,7 @@ class DroneCiService < CiService
end
def merge_request_valid?(data)
- ['opened', 'reopened'].include?(data[:object_attributes][:state]) &&
+ %w(opened reopened).include?(data[:object_attributes][:state]) &&
data[:object_attributes][:merge_status] == 'unchecked'
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 72da219df28..8b181221bb0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -6,16 +6,16 @@ class HipchatService < Service
a b i strong em br img pre code
table th tr td caption colgroup col thead tbody tfoot
ul ol li dl dt dd
- ]
+ ].freeze
prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_builds, :notify
+ boolean_accessor :notify_only_broken_pipelines, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -36,17 +36,17 @@ class HipchatService < Service
{ type: 'text', name: 'token', placeholder: 'Room token' },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
- { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] },
+ { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
{ type: 'text', name: 'api_version',
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push pipeline)
end
def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
- when "build"
- create_build_message(data) if should_build_be_notified?(data)
+ when "pipeline"
+ create_pipeline_message(data) if should_pipeline_be_notified?(data)
end
end
@@ -240,28 +240,29 @@ class HipchatService < Service
message
end
- def create_build_message(data)
- ref_type = data[:tag] ? 'tag' : 'branch'
- ref = data[:ref]
- sha = data[:sha]
- user_name = data[:commit][:author_name]
- status = data[:commit][:status]
- duration = data[:commit][:duration]
+ def create_pipeline_message(data)
+ pipeline_attributes = data[:object_attributes]
+ pipeline_id = pipeline_attributes[:id]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ ref = pipeline_attributes[:ref]
+ user_name = (data[:user] && data[:user][:name]) || 'API'
+ status = pipeline_attributes[:status]
+ duration = pipeline_attributes[:duration]
branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
def message_color(data)
- build_status_color(data) || color || 'yellow'
+ pipeline_status_color(data) || color || 'yellow'
end
- def build_status_color(data)
- return unless data && data[:object_kind] == 'build'
+ def pipeline_status_color(data)
+ return unless data && data[:object_kind] == 'pipeline'
- case data[:commit][:status]
+ case data[:object_attributes][:status]
when 'success'
'green'
else
@@ -294,10 +295,10 @@ class HipchatService < Service
end
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
when 'success'
- !notify_only_broken_builds?
+ !notify_only_broken_pipelines?
when 'failed'
true
else
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 5d6862d9faa..c62bb4fa120 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -33,7 +33,8 @@ class IrkerService < Service
end
def settings
- { server_host: server_host.present? ? server_host : 'localhost',
+ {
+ server_host: server_host.present? ? server_host : 'localhost',
server_port: server_port.present? ? server_port : 6659
}
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 9e65fdbf9d6..50435b67eda 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,4 +1,6 @@
class IssueTrackerService < Service
+ validate :one_issue_tracker, if: :activated?, on: :manual_change
+
default_value_for :category, 'issue_tracker'
# Pattern used to extract links from comments
@@ -92,4 +94,13 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
+
+ def one_issue_tracker
+ return if template?
+ return if project.blank?
+
+ if project.services.external_issue_trackers.where.not(id: id).any?
+ errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time')
+ end
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f2f019c43bb..02fbd5497fa 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -3,7 +3,7 @@ class KubernetesService < DeploymentService
include Gitlab::Kubernetes
include ReactiveCaching
- self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
@@ -36,7 +36,7 @@ class KubernetesService < DeploymentService
def initialize_properties
if properties.nil?
self.properties = {}
- self.namespace = project.path if project.present?
+ self.namespace = "#{project.path}-#{project.id}" if project.present?
end
end
@@ -62,23 +62,19 @@ class KubernetesService < DeploymentService
{ type: 'text',
name: 'namespace',
title: 'Kubernetes namespace',
- placeholder: 'Kubernetes namespace',
- },
+ placeholder: 'Kubernetes namespace' },
{ type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Kubernetes API URL, like https://kube.example.com/',
- },
+ placeholder: 'Kubernetes API URL, like https://kube.example.com/' },
{ type: 'text',
name: 'token',
title: 'Service token',
- placeholder: 'Service token',
- },
+ placeholder: 'Service token' },
{ type: 'textarea',
name: 'ca_pem',
title: 'Custom CA bundle',
- placeholder: 'Certificate Authority bundle (PEM format)',
- },
+ placeholder: 'Certificate Authority bundle (PEM format)' },
]
end
@@ -98,7 +94,12 @@ class KubernetesService < DeploymentService
{ key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true }
]
- variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
+
+ if ca_pem.present?
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
+ variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ end
+
variables
end
@@ -167,7 +168,7 @@ class KubernetesService < DeploymentService
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
- url.path = [ prefix, *parts ].join("/")
+ url.path = [prefix, *parts].join("/")
url.to_s
end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index 4ebc5318da1..1156d050622 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service:
<ol>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
- <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
- <li>Select events below to enable notifications. The channel and username are optional. </li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
+ <li>Paste the webhook <strong>URL</strong> into the field below.</li>
+ <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
@@ -28,14 +28,13 @@ class MattermostService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
+ { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
- "town-square"
+ "Channel handle (e.g. town-square)"
end
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
new file mode 100644
index 00000000000..a8d581a1f67
--- /dev/null
+++ b/app/models/project_services/mock_ci_service.rb
@@ -0,0 +1,82 @@
+# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
+class MockCiService < CiService
+ ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
+
+ prop_accessor :mock_service_url
+ validates :mock_service_url, presence: true, url: true, if: :activated?
+
+ def title
+ 'MockCI'
+ end
+
+ def description
+ 'Mock an external CI'
+ end
+
+ def self.to_param
+ 'mock_ci'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'mock_service_url',
+ placeholder: 'http://localhost:4004' },
+ ]
+ end
+
+ # Return complete url to build page
+ #
+ # Ex.
+ # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
+ #
+ def build_page(sha, ref)
+ url = [mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}"]
+
+ URI.join(*url).to_s
+ end
+
+ # Return string with build status or :error symbol
+ #
+ # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+ #
+ #
+ # Ex.
+ # @service.commit_status('13be4ac', 'master')
+ # # => 'success'
+ #
+ # @service.commit_status('2abe4ac', 'dev')
+ # # => 'running'
+ #
+ #
+ def commit_status(sha, ref)
+ response = HTTParty.get(commit_status_path(sha), verify: false)
+ read_commit_status(response)
+ rescue Errno::ECONNREFUSED
+ :error
+ end
+
+ def commit_status_path(sha)
+ url = [mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}.json"]
+
+ URI.join(*url).to_s
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'pending'
+ else
+ response['status']
+ end
+
+ if status.present? && ALLOWED_STATES.include?(status)
+ status
+ else
+ :error
+ end
+ end
+end
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
new file mode 100644
index 00000000000..ea585721e8f
--- /dev/null
+++ b/app/models/project_services/monitoring_service.rb
@@ -0,0 +1,16 @@
+# Base class for monitoring services
+#
+# These services integrate with a deployment solution like Prometheus
+# to provide additional features for environments.
+class MonitoringService < Service
+ default_value_for :category, 'monitoring'
+
+ def self.supported_events
+ %w()
+ end
+
+ # Environments have a number of metrics
+ def metrics(environment)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 9cc642591f4..d86f4f6f448 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,7 @@
class PivotaltrackerService < Service
include HTTParty
- API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+ API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze
prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
new file mode 100644
index 00000000000..375966b9efc
--- /dev/null
+++ b/app/models/project_services/prometheus_service.rb
@@ -0,0 +1,93 @@
+class PrometheusService < MonitoringService
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_lifetime = 1.minute
+
+ # Access to prometheus is directly through the API
+ prop_accessor :api_url
+
+ with_options presence: true, if: :activated? do
+ validates :api_url, url: true
+ end
+
+ after_save :clear_reactive_cache!
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ end
+ end
+
+ def title
+ 'Prometheus'
+ end
+
+ def description
+ 'Prometheus monitoring'
+ end
+
+ def help
+ 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
+ end
+
+ def self.to_param
+ 'prometheus'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ]
+ end
+
+ # Check we can connect to the Prometheus API
+ def test(*args)
+ client.ping
+
+ { success: true, result: 'Checked API endpoint' }
+ rescue Gitlab::PrometheusError => err
+ { success: false, result: err }
+ end
+
+ def metrics(environment)
+ with_reactive_cache(environment.slug) do |data|
+ data
+ end
+ end
+
+ # Cache metrics for specific environment
+ def calculate_reactive_cache(environment_slug)
+ return unless active? && project && !project.pending_delete?
+
+ memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
+ cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+
+ {
+ success: true,
+ metrics: {
+ # Memory used in MB
+ memory_values: client.query_range(memory_query, start: 8.hours.ago),
+ memory_current: client.query(memory_query),
+ # CPU Usage rate in cores.
+ cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
+ cpu_current: client.query(cpu_query)
+ },
+ last_update: Time.now.utc
+ }
+
+ rescue Gitlab::PrometheusError => err
+ { success: false, result: err.message }
+ end
+
+ def client
+ @prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
+ end
+end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index a963d27a376..3e618a8dbf1 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -29,25 +29,24 @@ class PushoverService < Service
['Normal Priority', 0],
['High Priority', 1]
],
- default_choice: 0
- },
+ default_choice: 0 },
{ type: 'select', name: 'sound', choices:
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
- ['Bike', 'bike'],
- ['Bugle', 'bugle'],
+ %w(Bike bike),
+ %w(Bugle bugle),
['Cash Register', 'cashregister'],
- ['Classical', 'classical'],
- ['Cosmic', 'cosmic'],
- ['Falling', 'falling'],
- ['Gamelan', 'gamelan'],
- ['Incoming', 'incoming'],
- ['Intermission', 'intermission'],
- ['Magic', 'magic'],
- ['Mechanical', 'mechanical'],
+ %w(Classical classical),
+ %w(Cosmic cosmic),
+ %w(Falling falling),
+ %w(Gamelan gamelan),
+ %w(Incoming incoming),
+ %w(Intermission intermission),
+ %w(Magic magic),
+ %w(Mechanical mechanical),
['Piano Bar', 'pianobar'],
- ['Siren', 'siren'],
+ %w(Siren siren),
['Space Alarm', 'spacealarm'],
['Tug Boat', 'tugboat'],
['Alien Alarm (long)', 'alien'],
@@ -56,8 +55,7 @@ class PushoverService < Service
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
- ]
- },
+ ] },
]
end
@@ -72,13 +70,14 @@ class PushoverService < Service
before = data[:before]
after = data[:after]
- if Gitlab::Git.blank_ref?(before)
- message = "#{data[:user_name]} pushed new branch \"#{ref}\"."
- elsif Gitlab::Git.blank_ref?(after)
- message = "#{data[:user_name]} deleted branch \"#{ref}\"."
- else
- message = "#{data[:user_name]} push to branch \"#{ref}\"."
- end
+ message =
+ if Gitlab::Git.blank_ref?(before)
+ "#{data[:user_name]} pushed new branch \"#{ref}\"."
+ elsif Gitlab::Git.blank_ref?(after)
+ "#{data[:user_name]} deleted branch \"#{ref}\"."
+ else
+ "#{data[:user_name]} push to branch \"#{ref}\"."
+ end
if data[:total_commits_count] > 0
message << "\nTotal commits count: #{data[:total_commits_count]}"
@@ -97,7 +96,7 @@ class PushoverService < Service
# Sound parameter MUST NOT be sent to API if not selected
if sound
- pushover_data.merge!(sound: sound)
+ pushover_data[:sound] = sound
end
PushoverService.post('/messages.json', body: pushover_data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index f77d2d7c60b..b657db6f9ee 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help
'This service sends notifications about projects events to Slack channels.<br />
- To setup this service:
+ To set up this service:
<ol>
- <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
- <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
- <li>Select events below to enable notifications. The channel and username are optional. </li>
+ <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
+ <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
@@ -27,14 +27,13 @@ class SlackService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
+ { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
- "#general"
+ "Channel name (e.g. general)"
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 06abd406523..aeaf63abab9 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,7 +4,7 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
- STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
+ STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
def total_repository_size
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index d0b991db112..70eef359cdd 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -5,9 +5,9 @@ class ProjectWiki
'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
- } unless defined?(MARKUPS)
+ }.freeze unless defined?(MARKUPS)
- class CouldNotCreateWikiError < StandardError; end
+ CouldNotCreateWikiError = Class.new(StandardError)
# Returns a string describing what went wrong after
# an operation fails.
@@ -19,6 +19,9 @@ class ProjectWiki
@user = user
end
+ delegate :empty?, to: :pages
+ delegate :repository_storage_path, to: :project
+
def path
@project.path + '.wiki'
end
@@ -39,8 +42,11 @@ class ProjectWiki
url_to_repo
end
- def http_url_to_repo
- [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
+ def http_url_to_repo(user = nil)
+ url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+ Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
end
def wiki_base_path
@@ -60,10 +66,6 @@ class ProjectWiki
!!repository.exists?
end
- def empty?
- pages.empty?
- end
-
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages
@@ -160,10 +162,6 @@ class ProjectWiki
}
end
- def repository_storage_path
- project.repository_storage_path
- end
-
private
def init_repo(path_with_namespace)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 6240912a6e1..39e979ef15b 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -8,8 +8,8 @@ class ProtectedBranch < ActiveRecord::Base
has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy
- validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
- validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+ validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
+ validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
accepts_nested_attributes_for :push_access_levels
accepts_nested_attributes_for :merge_access_levels
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 56c582cd9be..6ab04440ca8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -6,6 +6,7 @@ class Repository
attr_accessor :path_with_namespace, :project
CommitError = Class.new(StandardError)
+ CreateTreeError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
@@ -18,7 +19,7 @@ class Repository
CACHED_METHODS = %i(size commit_count readme version contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? empty? root_ref)
+ tag_count avatar exists? empty? root_ref).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
@@ -33,7 +34,7 @@ class Repository
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar
- }
+ }.freeze
# Wraps around the given method and caches its output in Redis and an instance
# variable.
@@ -49,10 +50,6 @@ class Repository
end
end
- def self.storages
- Gitlab.config.repositories.storages
- end
-
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -109,9 +106,7 @@ class Repository
offset: offset,
after: after,
before: before,
- # --follow doesn't play well with --skip. See:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- follow: false,
+ follow: path.present?,
skip_merges: skip_merges
}
@@ -317,11 +312,13 @@ class Repository
if !branch_name || branch_name == root_ref
branches.each do |branch|
cache.expire(:"diverging_commit_counts_#{branch.name}")
+ cache.expire(:"commit_count_#{branch.name}")
end
# In case a commit is pushed to a non-root branch we only have to flush the
# cache for said branch.
else
cache.expire(:"diverging_commit_counts_#{branch_name}")
+ cache.expire(:"commit_count_#{branch_name}")
end
end
@@ -487,9 +484,7 @@ class Repository
end
cache_method :exists?
- def empty?
- raw_repository.empty?
- end
+ delegate :empty?, to: :raw_repository
cache_method :empty?
# The size of this repository in megabytes.
@@ -503,14 +498,22 @@ class Repository
end
cache_method :commit_count, fallback: 0
+ def commit_count_for_ref(ref)
+ return 0 unless exists?
+
+ begin
+ cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) }
+ rescue Rugged::ReferenceError
+ 0
+ end
+ end
+
def branch_names
branches.map(&:name)
end
cache_method :branch_names, fallback: []
- def tag_names
- raw_repository.tag_names
- end
+ delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
def branch_count
@@ -750,136 +753,63 @@ class Repository
@tags ||= raw_repository.tags
end
- # rubocop:disable Metrics/ParameterLists
- def commit_dir(
- user, path,
- message:, branch_name:,
- author_email: nil, author_name: nil,
- start_branch_name: nil, start_project: project)
- check_tree_entry_for_dir(branch_name, path)
-
- if start_branch_name
- start_project.repository.
- check_tree_entry_for_dir(start_branch_name, path)
- end
+ def create_dir(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create_dir, file_path: path }]
- commit_file(
- user,
- "#{path}/.gitkeep",
- '',
- message: message,
- branch_name: branch_name,
- update: false,
- author_email: author_email,
- author_name: author_name,
- start_branch_name: start_branch_name,
- start_project: start_project)
+ multi_action(**options)
end
- # rubocop:enable Metrics/ParameterLists
- # rubocop:disable Metrics/ParameterLists
- def commit_file(
- user, path, content,
- message:, branch_name:, update: true,
- author_email: nil, author_name: nil,
- start_branch_name: nil, start_project: project)
- unless update
- error_message = "Filename already exists; update not allowed"
+ def create_file(user, path, content, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create, file_path: path, content: content }]
- if tree_entry_at(branch_name, path)
- raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
- end
+ multi_action(**options)
+ end
- if start_branch_name &&
- start_project.repository.tree_entry_at(start_branch_name, path)
- raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
- end
- end
+ def update_file(user, path, content, **options)
+ previous_path = options.delete(:previous_path)
+ action = previous_path && previous_path != path ? :move : :update
- multi_action(
- user: user,
- message: message,
- branch_name: branch_name,
- author_email: author_email,
- author_name: author_name,
- start_branch_name: start_branch_name,
- start_project: start_project,
- actions: [{ action: :create,
- file_path: path,
- content: content }])
- end
- # rubocop:enable Metrics/ParameterLists
+ options[:user] = user
+ options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
- # rubocop:disable Metrics/ParameterLists
- def update_file(
- user, path, content,
- message:, branch_name:, previous_path:,
- author_email: nil, author_name: nil,
- start_branch_name: nil, start_project: project)
- action = if previous_path && previous_path != path
- :move
- else
- :update
- end
-
- multi_action(
- user: user,
- message: message,
- branch_name: branch_name,
- author_email: author_email,
- author_name: author_name,
- start_branch_name: start_branch_name,
- start_project: start_project,
- actions: [{ action: action,
- file_path: path,
- content: content,
- previous_path: previous_path }])
+ multi_action(**options)
end
- # rubocop:enable Metrics/ParameterLists
- # rubocop:disable Metrics/ParameterLists
- def remove_file(
- user, path,
- message:, branch_name:,
- author_email: nil, author_name: nil,
- start_branch_name: nil, start_project: project)
- multi_action(
- user: user,
- message: message,
- branch_name: branch_name,
- author_email: author_email,
- author_name: author_name,
- start_branch_name: start_branch_name,
- start_project: start_project,
- actions: [{ action: :delete,
- file_path: path }])
+ def delete_file(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :delete, file_path: path }]
+
+ multi_action(**options)
end
- # rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
+
GitOperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_project: start_project) do |start_commit|
- index = rugged.index
- parents = if start_commit
- index.read_tree(start_commit.raw_commit.tree)
- [start_commit.sha]
- else
- []
- end
+ index = Gitlab::Git::Index.new(raw_repository)
- actions.each do |act|
- git_action(index, act)
+ if start_commit
+ index.read_tree(start_commit.raw_commit.tree)
+ parents = [start_commit.sha]
+ else
+ parents = []
+ end
+
+ actions.each do |options|
+ index.public_send(options.delete(:action), options)
end
options = {
- tree: index.write_tree(rugged),
+ tree: index.write_tree,
message: message,
parents: parents
}
@@ -892,7 +822,7 @@ class Repository
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
- author = Gitlab::Git::committer_hash(email: email, name: name) || committer
+ author = Gitlab::Git.committer_hash(email: email, name: name) || committer
{
author: author,
@@ -941,17 +871,18 @@ class Repository
end
def revert(
- user, commit, branch_name, revert_tree_id = nil,
+ user, commit, branch_name,
start_branch_name: nil, start_project: project)
- revert_tree_id ||= check_revert_content(commit, branch_name)
-
- return false unless revert_tree_id
-
GitOperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_project: start_project) do |start_commit|
+ revert_tree_id = check_revert_content(commit, start_commit.sha)
+ unless revert_tree_id
+ raise Repository::CreateTreeError.new('Failed to revert commit')
+ end
+
committer = user_to_committer(user)
Rugged::Commit.create(rugged,
@@ -964,17 +895,18 @@ class Repository
end
def cherry_pick(
- user, commit, branch_name, cherry_pick_tree_id = nil,
+ user, commit, branch_name,
start_branch_name: nil, start_project: project)
- cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
-
- return false unless cherry_pick_tree_id
-
GitOperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_project: start_project) do |start_commit|
+ cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
+ unless cherry_pick_tree_id
+ raise Repository::CreateTreeError.new('Failed to cherry-pick commit')
+ end
+
committer = user_to_committer(user)
Rugged::Commit.create(rugged,
@@ -998,9 +930,8 @@ class Repository
end
end
- def check_revert_content(target_commit, branch_name)
- source_sha = commit(branch_name).sha
- args = [target_commit.sha, source_sha]
+ def check_revert_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
args << { mainline: 1 } if target_commit.merge_commit?
revert_index = rugged.revert_commit(*args)
@@ -1012,9 +943,8 @@ class Repository
tree_id
end
- def check_cherry_pick_content(target_commit, branch_name)
- source_sha = commit(branch_name).sha
- args = [target_commit.sha, source_sha]
+ def check_cherry_pick_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
@@ -1074,6 +1004,8 @@ class Repository
end
def with_repo_branch_commit(start_repository, start_branch_name)
+ return yield(nil) if start_repository.empty_repo?
+
branch_name_or_sha =
if start_repository == self
start_branch_name
@@ -1170,30 +1102,6 @@ class Repository
blob_data_at(sha, '.gitlab-ci.yml')
end
- protected
-
- def tree_entry_at(branch_name, path)
- branch_exists?(branch_name) &&
- # tree_entry is private
- raw_repository.send(:tree_entry, commit(branch_name), path)
- end
-
- def check_tree_entry_for_dir(branch_name, path)
- return unless branch_exists?(branch_name)
-
- entry = tree_entry_at(branch_name, path)
-
- return unless entry
-
- if entry[:type] == :blob
- raise Gitlab::Git::Repository::InvalidBlobName.new(
- "Directory already exists as a file")
- else
- raise Gitlab::Git::Repository::InvalidBlobName.new(
- "Directory already exists")
- end
- end
-
private
def blob_data_at(sha, path)
@@ -1204,58 +1112,6 @@ class Repository
blob.data
end
- def git_action(index, action)
- path = normalize_path(action[:file_path])
-
- if action[:action] == :move
- previous_path = normalize_path(action[:previous_path])
- end
-
- case action[:action]
- when :create, :update, :move
- mode =
- case action[:action]
- when :update
- index.get(path)[:mode]
- when :move
- index.get(previous_path)[:mode]
- end
- mode ||= 0o100644
-
- index.remove(previous_path) if action[:action] == :move
-
- content = if action[:encoding] == 'base64'
- Base64.decode64(action[:content])
- else
- action[:content]
- end
-
- detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
-
- unless detect && detect[:type] == :binary
- # When writing to the repo directly as we are doing here,
- # the `core.autocrlf` config isn't taken into account.
- content.gsub!("\r\n", "\n") if self.autocrlf
- end
-
- oid = rugged.write(content, :blob)
-
- index.add(path: path, oid: oid, mode: mode)
- when :delete
- index.remove(path)
- end
- end
-
- def normalize_path(path)
- pathname = Gitlab::Git::PathHelper.normalize_path(path)
-
- if pathname.each_filename.include?('..')
- raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
- end
-
- pathname.to_s
- end
-
def refs_directory_exists?
return false unless path_with_namespace
diff --git a/app/models/route.rb b/app/models/route.rb
index 73574a6206b..41e6eb7cb73 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base
attributes[:path] = route.path.sub(path_was, path)
end
- if name_changed? && route.name.present?
+ if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index facaaf9b331..e73f7e5d1a3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -210,12 +210,11 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w[
+ service_names = %w[
asana
assembla
bamboo
buildkite
- builds_email
bugzilla
campfire
custom_issue_tracker
@@ -232,12 +231,16 @@ class Service < ActiveRecord::Base
mattermost
pipelines_email
pivotaltracker
+ prometheus
pushover
redmine
slack_slash_commands
slack
teamcity
]
+ service_names << 'mock_ci' if Rails.env.development?
+
+ service_names.sort_by(&:downcase)
end
def self.build_from_template(project_id, template)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2665a7249a3..dbd564e5e7d 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -120,7 +120,7 @@ class Snippet < ActiveRecord::Base
end
def visibility_level_field
- visibility_level
+ :visibility_level
end
def no_highlighting?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3dda7948d0b..da3fa7277c2 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -17,7 +17,7 @@ class Todo < ActiveRecord::Base
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed
- }
+ }.freeze
belongs_to :author, class_name: "User"
belongs_to :note
@@ -48,8 +48,14 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
class << self
+ # Priority sorting isn't displayed in the dropdown, because we don't show
+ # milestones, but still show something if the user has a URL with that
+ # selected.
def sort(method)
- method == "priority" ? order_by_labels_priority : order_by(method)
+ case method.to_s
+ when 'priority', 'label_priority' then order_by_labels_priority
+ else order_by(method)
+ end
end
# Order by priority depending on which issue/merge request the Todo belongs to
diff --git a/app/models/upload.rb b/app/models/upload.rb
new file mode 100644
index 00000000000..13987931b05
--- /dev/null
+++ b/app/models/upload.rb
@@ -0,0 +1,63 @@
+class Upload < ActiveRecord::Base
+ # Upper limit for foreground checksum processing
+ CHECKSUM_THRESHOLD = 100.megabytes
+
+ belongs_to :model, polymorphic: true
+
+ validates :size, presence: true
+ validates :path, presence: true
+ validates :model, presence: true
+ validates :uploader, presence: true
+
+ before_save :calculate_checksum, if: :foreground_checksum?
+ after_commit :schedule_checksum, unless: :foreground_checksum?
+
+ def self.remove_path(path)
+ where(path: path).destroy_all
+ end
+
+ def self.record(uploader)
+ remove_path(uploader.relative_path)
+
+ create(
+ size: uploader.file.size,
+ path: uploader.relative_path,
+ model: uploader.model,
+ uploader: uploader.class.to_s
+ )
+ end
+
+ def absolute_path
+ return path unless relative_path?
+
+ uploader_class.absolute_path(self)
+ end
+
+ def calculate_checksum
+ return unless exist?
+
+ self.checksum = Digest::SHA256.file(absolute_path).hexdigest
+ end
+
+ def exist?
+ File.exist?(absolute_path)
+ end
+
+ private
+
+ def foreground_checksum?
+ size <= CHECKSUM_THRESHOLD
+ end
+
+ def schedule_checksum
+ UploadChecksumWorker.perform_async(id)
+ end
+
+ def relative_path?
+ !path.start_with?('/')
+ end
+
+ def uploader_class
+ Object.const_get(uploader)
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index f614eb66e1f..8c7ad5d5174 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,6 +21,7 @@ class User < ActiveRecord::Base
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
+ default_value_for :project_view, :files
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -81,7 +82,6 @@ class User < ActiveRecord::Base
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
- has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id
@@ -95,16 +95,22 @@ class User < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, dependent: :destroy
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+ # Issues that a user owns are expected to be moved to the "ghost" user before
+ # the user is destroyed. If the user owns any issues during deletion, this
+ # should be treated as an exceptional condition.
+ has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id
+
#
# Validations
#
# Note: devise :validatable above adds validations for :email and :password
validates :name, presence: true
- validates_confirmation_of :email
+ validates :email, confirmation: true
validates :notification_email, presence: true
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
@@ -184,6 +190,7 @@ class User < ActiveRecord::Base
end
mount_uploader :avatar, AvatarUploader
+ has_many :uploads, as: :model, dependent: :destroy
# Scopes
scope :admins, -> { where(admin: true) }
@@ -317,8 +324,7 @@ class User < ActiveRecord::Base
end
def find_by_personal_access_token(token_string)
- personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
- personal_access_token&.user
+ PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
# Returns a user for the given SSH key.
@@ -334,9 +340,34 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR})
+ (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
}x
end
+
+ # Return (create if necessary) the ghost user. The ghost user
+ # owns records previously belonging to deleted users.
+ def ghost
+ unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u|
+ u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
+ u.name = 'Ghost User'
+ end
+ end
+ end
+
+ def self.internal_attributes
+ [:ghost]
+ end
+
+ def internal?
+ self.class.internal_attributes.any? { |a| self[a] }
+ end
+
+ def self.internal
+ where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
+ end
+
+ def self.non_internal
+ where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
end
#
@@ -457,7 +488,7 @@ class User < ActiveRecord::Base
Group.member_descendants(id)
end
- def nested_projects
+ def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
@@ -540,14 +571,14 @@ class User < ActiveRecord::Base
end
def can_create_group?
- can?(:create_group, nil)
+ can?(:create_group)
end
def can_select_namespace?
several_namespaces? || admin
end
- def can?(action, subject)
+ def can?(action, subject = :global)
Ability.allowed?(self, action, subject)
end
@@ -580,8 +611,8 @@ class User < ActiveRecord::Base
if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
- where(source_project_id: project.id,
- source_branch: event.branch_name)
+ where(source_project_id: project.id,
+ source_branch: event.branch_name)
merge_requests.empty?
end
end
@@ -846,7 +877,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.
- where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+ where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
@@ -932,6 +963,14 @@ class User < ActiveRecord::Base
self.admin = (new_level == 'admin')
end
+ protected
+
+ # override, from Devise::Validatable
+ def password_required?
+ return false if internal?
+ super
+ end
+
private
def ci_projects_union
@@ -999,4 +1038,43 @@ class User < ActiveRecord::Base
super
end
end
+
+ def self.unique_internal(scope, username, email_pattern, &b)
+ scope.first || create_unique_internal(scope, username, email_pattern, &b)
+ end
+
+ def self.create_unique_internal(scope, username, email_pattern, &creation_block)
+ # Since we only want a single one of these in an instance, we use an
+ # exclusive lease to ensure than this block is never run concurrently.
+ lease_key = "user:unique_internal:#{username}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ # Recheck if the user is already present. One might have been
+ # added between the time we last checked (first line of this method)
+ # and the time we acquired the lock.
+ existing_user = uncached { scope.first }
+ return existing_user if existing_user.present?
+
+ uniquify = Uniquify.new
+
+ username = uniquify.string(username) { |s| User.find_by_username(s) }
+
+ email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
+ User.find_by_email(s)
+ end
+
+ scope.create(
+ username: username,
+ email: email,
+ &creation_block
+ )
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2caebb496db..c771c22f46a 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -149,7 +149,13 @@ class WikiPage
end
# Returns boolean True or False if this instance
- # has been fully saved to disk or not.
+ # is the latest commit version of the page.
+ def latest?
+ !historical?
+ end
+
+ # Returns boolean True or False if this instance
+ # has been fully created on disk or not.
def persisted?
@persisted == true
end
@@ -220,6 +226,8 @@ class WikiPage
end
def save(method, *args)
+ saved = false
+
project_wiki = wiki
if valid? && project_wiki.send(method, *args)
@@ -237,10 +245,10 @@ class WikiPage
set_attributes
@persisted = true
+ saved = true
else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message
- @persisted = false
end
- @persisted
+ saved
end
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index b9f1c29c32e..8890409d056 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -6,14 +6,16 @@ class BasePolicy
@cannot_set = cannot_set
end
- def size
- to_set.size
- end
+ delegate :size, to: :to_set
def self.empty
new(Set.new, Set.new)
end
+ def self.none
+ empty.freeze
+ end
+
def can?(ability)
@can_set.include?(ability) && !@cannot_set.include?(ability)
end
@@ -51,7 +53,8 @@ class BasePolicy
end
def self.class_for(subject)
- return GlobalPolicy if subject.nil?
+ return GlobalPolicy if subject == :global
+ raise ArgumentError, 'no policy for nil' if subject.nil?
if subject.class.try(:presenter?)
subject = subject.subject
@@ -81,7 +84,7 @@ class BasePolicy
end
def abilities
- return RuleSet.empty if @user && @user.blocked?
+ return RuleSet.none if @user && @user.blocked?
return anonymous_abilities if @user.nil?
collect_rules { rules }
end
diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb
new file mode 100644
index 00000000000..c90c9ac0583
--- /dev/null
+++ b/app/policies/ci/trigger_policy.rb
@@ -0,0 +1,13 @@
+module Ci
+ class TriggerPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+
+ if can?(:admin_build)
+ can! :admin_trigger if @subject.owner.blank? ||
+ @subject.owner == @user
+ can! :manage_trigger
+ end
+ end
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 3c2fbe6b56b..cb72c2b4590 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy
can! :create_group if @user.can_create_group
can! :read_users_list
+
+ unless @user.blocked? || @user.internal?
+ can! :log_in unless @user.access_locked?
+ can! :access_api
+ can! :access_git
+ can! :receive_notifications
+ end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 0be6e113655..4cc21696eb6 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -33,8 +33,6 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
-
- additional_rules!(master)
end
def can_read_group?
@@ -45,8 +43,4 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any?
end
-
- def additional_rules!(master)
- # This is meant to be overriden in EE
- end
end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 03a2499e263..229846e368c 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -3,6 +3,14 @@ class UserPolicy < BasePolicy
def rules
can! :read_user if @user || !restricted_public_level?
+
+ if @user
+ if @user.admin? || @subject == @user
+ can! :destroy_user
+ end
+
+ cannot! :destroy_user if @subject.ghost?
+ end
end
def restricted_public_level?
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
new file mode 100644
index 00000000000..86ac513b3c0
--- /dev/null
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -0,0 +1,60 @@
+module Projects
+ module Settings
+ class DeployKeysPresenter < Gitlab::View::Presenter::Simple
+ presents :project
+ delegate :size, to: :enabled_keys, prefix: true
+ delegate :size, to: :available_project_keys, prefix: true
+ delegate :size, to: :available_public_keys, prefix: true
+
+ def new_key
+ @key ||= DeployKey.new
+ end
+
+ def enabled_keys
+ @enabled_keys ||= project.deploy_keys
+ end
+
+ def any_keys_enabled?
+ enabled_keys.any?
+ end
+
+ def available_keys
+ @available_keys ||= current_user.accessible_deploy_keys - enabled_keys
+ end
+
+ def available_project_keys
+ @available_project_keys ||= current_user.project_deploy_keys - enabled_keys
+ end
+
+ def any_available_project_keys_enabled?
+ available_project_keys.any?
+ end
+
+ def key_available?(deploy_key)
+ available_keys.include?(deploy_key)
+ end
+
+ def available_public_keys
+ return @available_public_keys if defined?(@available_public_keys)
+
+ @available_public_keys ||= DeployKey.are_public - enabled_keys
+
+ # Public keys that are already used by another accessible project are already
+ # in @available_project_keys.
+ @available_public_keys -= available_project_keys
+ end
+
+ def any_available_public_keys_enabled?
+ available_public_keys.any?
+ end
+
+ def to_partial_path
+ 'projects/deploy_keys/index'
+ end
+
+ def form_partial_path
+ 'projects/deploy_keys/form'
+ end
+ end
+ end
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index b5384e6462b..5bcbe285052 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity
path_to(:retry_namespace_project_build, build)
end
- expose :play_path, if: ->(build, _) { build.manual? } do |build|
+ expose :play_path, if: ->(build, _) { build.playable? } do |build|
path_to(:play_namespace_project_build, build)
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 7445298c714..5f80ab397a9 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -6,7 +6,7 @@ class MergeRequestEntity < IssuableEntity
expose :merge_params
expose :merge_status
expose :merge_user_id
- expose :merge_when_build_succeeds
+ expose :merge_when_pipeline_succeeds
expose :source_branch
expose :source_project_id
expose :target_branch
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 2bc6cf3266e..ab2d3d5a3ec 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,5 +1,5 @@
class PipelineSerializer < BaseSerializer
- class InvalidResourceError < StandardError; end
+ InvalidResourceError = Class.new(StandardError)
entity PipelineEntity
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index ddaaed90e5b..b2a543daa00 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -1,10 +1,16 @@
-AccessTokenValidationService = Struct.new(:token) do
+class AccessTokenValidationService
# Results:
VALID = :valid
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
+ attr_reader :token
+
+ def initialize(token)
+ @token = token
+ end
+
def validate(scopes: [])
if token.expired?
return EXPIRED
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 5b2fcdf3b16..08fe6e3293e 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -2,7 +2,7 @@ module Auth
class ContainerRegistryAuthenticationService < BaseService
include Gitlab::CurrentSettings
- AUDIENCE = 'container_registry'
+ AUDIENCE = 'container_registry'.freeze
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index fa45506317e..745c2c4b681 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -28,9 +28,7 @@ class BaseService
SystemHooksService.new
end
- def repository
- project.repository
- end
+ delegate :repository, to: :project
# Add an error to the specified model for restricted visibility levels
def deny_visibility_level(model, denied_visibility_level = nil)
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 8a94c54b6ab..83f51947bd4 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -5,7 +5,7 @@ module Boards
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list?
- issues
+ issues.order_by_position_and_priority
end
private
@@ -26,7 +26,6 @@ module Boards
def filter_params
set_default_scope
- set_default_sort
set_project
set_state
@@ -37,10 +36,6 @@ module Boards
params[:scope] = 'all'
end
- def set_default_sort
- params[:sort] = 'priority'
- end
-
def set_project
params[:project_id] = project.id
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 96554a92a02..2a9981ab884 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false unless valid_move?
+ return false if issue_params.empty?
update_service.execute(issue)
end
@@ -14,7 +14,7 @@ module Boards
@board ||= project.boards.find(params[:board_id])
end
- def valid_move?
+ def move_between_lists?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
end
@@ -32,11 +32,19 @@ module Boards
end
def issue_params
- {
- add_label_ids: add_label_ids,
- remove_label_ids: remove_label_ids,
- state_event: issue_state
- }
+ attrs = {}
+
+ if move_between_lists?
+ attrs.merge!(
+ add_label_ids: add_label_ids,
+ remove_label_ids: remove_label_ids,
+ state_event: issue_state,
+ )
+ end
+
+ attrs[:move_between_iids] = move_between_iids if move_between_iids
+
+ attrs
end
def issue_state
@@ -58,6 +66,12 @@ module Boards
Array(label_ids).compact
end
+
+ def move_between_iids
+ return unless params[:move_after_iid] || params[:move_before_iid]
+
+ [params[:move_after_iid], params[:move_before_iid]]
+ end
end
end
end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index b7da3f8e7eb..70fb2c5e38f 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,9 +10,7 @@ module Ci
end
end
- def project
- pipeline.project
- end
+ delegate :project, to: :pipeline
private
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index e3bc9847200..38a85e9fc42 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -59,7 +59,8 @@ module Ci
private
def skip_ci?
- pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
+ return false unless pipeline.git_commit_message
+ pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def commit
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 6af3c1ca5b1..dca5aa9f5d7 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -3,7 +3,7 @@ module Ci
def execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables)
- pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+ pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
execute(ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted?
trigger_request
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
deleted file mode 100644
index 240ddabec36..00000000000
--- a/app/services/ci/image_for_build_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Ci
- class ImageForBuildService
- def execute(project, opts)
- ref = opts[:ref]
- sha = opts[:sha] || ref_sha(project, ref)
- pipelines = project.pipelines.where(sha: sha)
-
- image_name = image_for_status(pipelines.latest_status(ref))
- image_path = Rails.root.join('public/ci', image_name)
-
- OpenStruct.new(path: image_path, name: image_name)
- end
-
- private
-
- def ref_sha(project, ref)
- project.commit(ref).try(:sha) if ref
- end
-
- def image_for_status(status)
- status ||= 'unknown'
- 'build-' + status + ".svg"
- end
- end
-end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 79eb97b7b55..2935d00c075 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -22,6 +22,8 @@ module Ci
def process_stage(index)
current_status = status_for_prior_stages(index)
+ return if HasStatus::BLOCKED_STATUS == current_status
+
if HasStatus::COMPLETED_STATUSES.include?(current_status)
created_builds_in_stage(index).select do |build|
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
@@ -33,7 +35,7 @@ module Ci
def process_build(build, current_status)
if valid_statuses_for_when(build.when).include?(current_status)
- build.enqueue
+ build.action? ? build.actionize : build.enqueue
true
else
build.skip
@@ -49,6 +51,8 @@ module Ci
%w[failed]
when 'always'
%w[success failed skipped]
+ when 'manual'
+ %w[success]
else
[]
end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
deleted file mode 100644
index 6f03bf2be13..00000000000
--- a/app/services/ci/register_build_service.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-module Ci
- # This class responsible for assigning
- # proper pending build to runner on runner API request
- class RegisterBuildService
- include Gitlab::CurrentSettings
-
- attr_reader :runner
-
- Result = Struct.new(:build, :valid?)
-
- def initialize(runner)
- @runner = runner
- end
-
- def execute
- builds =
- if runner.shared?
- builds_for_shared_runner
- else
- builds_for_specific_runner
- end
-
- build = builds.find do |build|
- runner.can_pick?(build)
- end
-
- if build
- # In case when 2 runners try to assign the same build, second runner will be declined
- # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
- build.runner_id = runner.id
- build.run!
- end
-
- Result.new(build, true)
-
- rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
- Result.new(build, false)
- end
-
- private
-
- def builds_for_shared_runner
- new_builds.
- # don't run projects which have not enabled shared runners and builds
- joins(:project).where(projects: { shared_runners_enabled: true }).
- joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
- where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
-
- # Implement fair scheduling
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
- order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
- end
-
- def builds_for_specific_runner
- new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC')
- end
-
- def running_builds_for_shared_runners
- Ci::Build.running.where(runner: Ci::Runner.shared).
- group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
- end
-
- def new_builds
- Ci::Build.pending.unstarted
- end
-
- def shared_runner_build_limits_feature_enabled?
- ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
- end
- end
-end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
new file mode 100644
index 00000000000..d6a4280ce4c
--- /dev/null
+++ b/app/services/ci/register_job_service.rb
@@ -0,0 +1,85 @@
+module Ci
+ # This class responsible for assigning
+ # proper pending build to runner on runner API request
+ class RegisterJobService
+ include Gitlab::CurrentSettings
+
+ attr_reader :runner
+
+ Result = Struct.new(:build, :valid?)
+
+ def initialize(runner)
+ @runner = runner
+ end
+
+ def execute
+ builds =
+ if runner.shared?
+ builds_for_shared_runner
+ else
+ builds_for_specific_runner
+ end
+
+ valid = true
+
+ builds.find do |build|
+ next unless runner.can_pick?(build)
+
+ begin
+ # In case when 2 runners try to assign the same build, second runner will be declined
+ # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
+ build.runner_id = runner.id
+ build.run!
+
+ return Result.new(build, true)
+ rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
+ # We are looping to find another build that is not conflicting
+ # It also indicates that this build can be picked and passed to runner.
+ # If we don't do it, basically a bunch of runners would be competing for a build
+ # and thus we will generate a lot of 409. This will increase
+ # the number of generated requests, also will reduce significantly
+ # how many builds can be picked by runner in a unit of time.
+ # In case we hit the concurrency-access lock,
+ # we still have to return 409 in the end,
+ # to make sure that this is properly handled by runner.
+ valid = false
+ end
+ end
+
+ Result.new(nil, valid)
+ end
+
+ private
+
+ def builds_for_shared_runner
+ new_builds.
+ # don't run projects which have not enabled shared runners and builds
+ joins(:project).where(projects: { shared_runners_enabled: true }).
+ joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
+ where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
+
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
+ order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
+ end
+
+ def builds_for_specific_runner
+ new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC')
+ end
+
+ def running_builds_for_shared_runners
+ Ci::Build.running.where(runner: Ci::Runner.shared).
+ group(:project_id).select(:project_id, 'count(*) AS running_builds')
+ end
+
+ def new_builds
+ Ci::Build.pending.unstarted
+ end
+
+ def shared_runner_build_limits_feature_enabled?
+ ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
+ end
+ end
+end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 4b47ee489cf..89da05b72bb 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -1,17 +1,9 @@
module Ci
class RetryBuildService < ::BaseService
- CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name
- allow_failure stage stage_idx trigger_request
- yaml_variables when environment coverage_regex]
- .freeze
-
- REJECT_ATTRIBUTES = %i[id status user token coverage trace runner
- artifacts_file artifacts_metadata artifacts_size
- created_at updated_at started_at finished_at
- queued_at erased_by erased_at].freeze
-
- IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url
- deploy job_id description].freeze
+ CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
+ allow_failure stage stage_idx trigger_request
+ yaml_variables when environment coverage_regex
+ description tag_list].freeze
def execute(build)
reprocess(build).tap do |new_build|
@@ -30,7 +22,7 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- attributes = CLONE_ATTRIBUTES.map do |attribute|
+ attributes = CLONE_ACCESSORS.map do |attribute|
[attribute, build.send(attribute)]
end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 2c5e130e5aa..574561adc4c 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -1,5 +1,7 @@
module Ci
class RetryPipelineService < ::BaseService
+ include Gitlab::OptimisticLocking
+
def execute(pipeline)
unless can?(current_user, :update_pipeline, pipeline)
raise Gitlab::Access::AccessDeniedError
@@ -12,6 +14,10 @@ module Ci
.reprocess(build)
end
+ pipeline.builds.skipped.find_each do |skipped|
+ retry_optimistic_lock(skipped) { |build| build.process }
+ end
+
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close_all(pipeline)
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 25e22f14e60..1297a792259 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -1,16 +1,16 @@
module Commits
class ChangeService < ::BaseService
- class ValidationError < StandardError; end
- class ChangeError < StandardError; end
+ ValidationError = Class.new(StandardError)
+ ChangeError = Class.new(StandardError)
def execute
@start_project = params[:start_project] || @project
@start_branch = params[:start_branch]
@target_branch = params[:target_branch]
@commit = params[:commit]
- @create_merge_request = params[:create_merge_request].present?
- check_push_permissions unless @create_merge_request
+ check_push_permissions
+
commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ChangeError => ex
@@ -26,34 +26,21 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- if @create_merge_request
- into = @commit.public_send("#{action}_branch_name")
- tree_branch = @start_branch
- else
- into = tree_branch = @target_branch
- end
-
- tree_id = repository.public_send(
- "check_#{action}_content", @commit, tree_branch)
-
- if tree_id
- validate_target_branch(into) if @create_merge_request
+ validate_target_branch if different_branch?
- repository.public_send(
- action,
- current_user,
- @commit,
- into,
- tree_id,
- start_project: @start_project,
- start_branch_name: @start_branch)
+ repository.public_send(
+ action,
+ current_user,
+ @commit,
+ @target_branch,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
- success
- else
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
+ success
+ rescue Repository::CreateTreeError
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
- raise ChangeError, error_msg
- end
+ raise ChangeError, error_msg
end
def check_push_permissions
@@ -66,16 +53,17 @@ module Commits
true
end
- def validate_target_branch(new_branch)
- # Temporary branch exists and contains the change commit
- return if repository.find_branch(new_branch)
-
+ def validate_target_branch
result = ValidateNewBranchService.new(@project, current_user)
- .execute(new_branch)
+ .execute(@target_branch)
if result[:status] == :error
raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
end
end
+
+ def different_branch?
+ @start_branch != @target_branch || @start_project != @project
+ end
end
end
diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb
new file mode 100644
index 00000000000..297c7d696c3
--- /dev/null
+++ b/app/services/concerns/issues/resolve_discussions.rb
@@ -0,0 +1,32 @@
+module Issues
+ module ResolveDiscussions
+ attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id
+
+ def filter_resolve_discussion_params
+ @merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of)
+ @discussion_to_resolve_id ||= params.delete(:discussion_to_resolve)
+ end
+
+ def merge_request_to_resolve_discussions_of
+ return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of)
+
+ @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id).
+ execute.
+ find_by(iid: merge_request_to_resolve_discussions_of_iid)
+ end
+
+ def discussions_to_resolve
+ return [] unless merge_request_to_resolve_discussions_of
+
+ @discussions_to_resolve ||=
+ if discussion_to_resolve_id
+ discussion_or_nil = merge_request_to_resolve_discussions_of
+ .find_diff_discussion(discussion_to_resolve_id)
+ Array(discussion_or_nil)
+ else
+ merge_request_to_resolve_discussions_of
+ .resolvable_discussions
+ end
+ end
+ end
+end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 77459d8779d..b07338d500a 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,5 +1,7 @@
class CreateBranchService < BaseService
def execute(branch_name, ref)
+ create_master_branch if project.empty_repo?
+
result = ValidateNewBranchService.new(project, current_user)
.execute(branch_name)
@@ -19,4 +21,16 @@ class CreateBranchService < BaseService
def success(branch)
super().merge(branch: branch)
end
+
+ private
+
+ def create_master_branch
+ project.repository.commit_file(
+ current_user,
+ '/README.md',
+ '',
+ message: 'Add README.md',
+ branch_name: 'master',
+ update: false)
+ end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 0a25f56d24c..c8a60422bf4 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,6 +1,6 @@
module Files
class BaseService < ::BaseService
- class ValidationError < StandardError; end
+ ValidationError = Class.new(StandardError)
def execute
@start_project = params[:start_project] || @project
@@ -58,16 +58,12 @@ module Files
raise_error("You are not allowed to push into this branch")
end
- unless project.empty_repo?
- unless @start_project.repository.branch_exists?(@start_branch)
- raise_error('You can only create or edit files when you are on a branch')
- end
+ if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch)
+ raise ValidationError, 'You can only create or edit files when you are on a branch'
+ end
- if different_branch?
- if repository.branch_exists?(@target_branch)
- raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
- end
- end
+ if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
+ raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes"
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 858de5f0538..083ffdc634c 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,7 @@
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(
+ repository.create_dir(
current_user,
@file_path,
message: @commit_message,
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 88dd7bbaedb..65b5537fb68 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,13 +1,12 @@
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(
+ repository.create_file(
current_user,
@file_path,
@file_content,
message: @commit_message,
branch_name: @target_branch,
- update: false,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
@@ -17,6 +16,10 @@ module Files
def validate
super
+ if @file_content.nil?
+ raise_error("You must provide content.")
+ end
+
if @file_path =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name ' +
diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb
index c3be806a42d..e294659bc98 100644
--- a/app/services/files/destroy_service.rb
+++ b/app/services/files/destroy_service.rb
@@ -1,7 +1,7 @@
module Files
class DestroyService < Files::BaseService
def commit
- repository.remove_file(
+ repository.delete_file(
current_user,
@file_path,
message: @commit_message,
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index af6da5b9d56..700f9f4f6f0 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,6 +1,8 @@
module Files
class MultiService < Files::BaseService
- class FileChangedError < StandardError; end
+ FileChangedError = Class.new(StandardError)
+
+ ACTIONS = %w[create update delete move].freeze
def commit
repository.multi_action(
@@ -21,10 +23,19 @@ module Files
super
params[:actions].each_with_index do |action, index|
+ if ACTIONS.include?(action[:action].to_s)
+ action[:action] = action[:action].to_sym
+ else
+ raise_error("Unknown action type `#{action[:action]}`.")
+ end
+
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
+ action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+ action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
@@ -43,8 +54,6 @@ module Files
validate_delete(action)
when :move
validate_move(action, index)
- else
- raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
@@ -92,6 +101,20 @@ module Files
if repository.blob_at_branch(params[:branch], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
+
+ if action[:content].nil?
+ raise_error("You must provide content.")
+ end
+ end
+
+ def validate_update(action)
+ if action[:content].nil?
+ raise_error("You must provide content.")
+ end
+
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+ end
end
def validate_delete(action)
@@ -114,11 +137,5 @@ module Files
params[:actions][index][:content] = blob.data
end
end
-
- def validate_update(action)
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
- end
- end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index a71fe61a4b6..fbbab97632e 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,6 +1,6 @@
module Files
class UpdateService < Files::BaseService
- class FileChangedError < StandardError; end
+ FileChangedError = Class.new(StandardError)
def commit
repository.update_file(current_user, @file_path, @file_content,
@@ -18,6 +18,10 @@ module Files
def validate
super
+ if @file_content.nil?
+ raise_error("You must provide content.")
+ end
+
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
index 27bcc047601..ed6ea638235 100644
--- a/app/services/git_operation_service.rb
+++ b/app/services/git_operation_service.rb
@@ -56,12 +56,16 @@ class GitOperationService
start_project: repository.project,
&block)
- check_with_branch_arguments!(
- branch_name, start_branch_name, start_project)
+ start_repository = start_project.repository
+ start_branch_name = nil if start_repository.empty_repo?
+
+ if start_branch_name && !start_repository.branch_exists?(start_branch_name)
+ raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}"
+ end
update_branch_with_hooks(branch_name) do
repository.with_repo_branch_commit(
- start_project.repository,
+ start_repository,
start_branch_name || branch_name,
&block)
end
@@ -149,31 +153,4 @@ class GitOperationService
repository.raw_repository.autocrlf = :input
end
end
-
- def check_with_branch_arguments!(
- branch_name, start_branch_name, start_project)
- return if repository.branch_exists?(branch_name)
-
- if repository.project != start_project
- unless start_branch_name
- raise ArgumentError,
- 'Should also pass :start_branch_name if' +
- ' :start_project is different from current project'
- end
-
- unless start_project.repository.branch_exists?(start_branch_name)
- raise ArgumentError,
- "Cannot find branch #{branch_name} nor" \
- " #{start_branch_name} from" \
- " #{start_project.path_with_namespace}"
- end
- elsif start_branch_name
- unless repository.branch_exists?(start_branch_name)
- raise ArgumentError,
- "Cannot find branch #{branch_name} nor" \
- " #{start_branch_name} from" \
- " #{repository.project.path_with_namespace}"
- end
- end
- end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index dbe2fda27b5..bc7431c89a8 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -99,6 +99,8 @@ class GitPushService < BaseService
UpdateMergeRequestsWorker
.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
+ SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks)
+
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index febeb661fb5..c4e9b8fd8e0 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -2,6 +2,7 @@ module Groups
class CreateService < Groups::BaseService
def initialize(user, params = {})
@current_user, @params = user, params.dup
+ @chat_team = @params.delete(:create_chat_team)
end
def execute
@@ -20,9 +21,23 @@ module Groups
end
@group.name ||= @group.path.dup
+
+ if create_chat_team?
+ response = Mattermost::CreateTeamService.new(@group, current_user).execute
+ return @group if @group.errors.any?
+
+ @group.build_chat_team(name: response['name'], team_id: response['id'])
+ end
+
@group.save
@group.add_owner(current_user)
@group
end
+
+ private
+
+ def create_chat_team?
+ Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
+ end
end
end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 2e2d7f884ac..497fdb09cdc 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -18,7 +18,8 @@ module Groups
end
group.children.each do |group|
- DestroyService.new(group, current_user).async_execute
+ # This needs to be synchronous since the namespace gets destroyed below
+ DestroyService.new(group, current_user).execute
end
group.really_destroy!
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 9500faf2862..b071a398481 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -203,6 +203,7 @@ class IssuableBaseService < BaseService
change_state(issuable)
change_subscription(issuable)
change_todo(issuable)
+ toggle_award(issuable)
filter_params(issuable)
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
@@ -210,7 +211,7 @@ class IssuableBaseService < BaseService
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
- if params.present?
+ if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
before_update(issuable)
@@ -263,6 +264,14 @@ class IssuableBaseService < BaseService
end
end
+ def toggle_award(issuable)
+ award = params.delete(:emoji_award)
+ if award
+ todo_service.new_award_emoji(issuable, current_user)
+ issuable.toggle_award_emoji(award, current_user)
+ end
+ end
+
def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 35af867a098..ee1b40db718 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,13 +1,5 @@
module Issues
class BaseService < ::IssuableBaseService
- attr_reader :merge_request_for_resolving_discussions
-
- def initialize(*args)
- super
-
- @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
- end
-
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7cd927d8005..77bced4bd5c 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -1,50 +1,56 @@
module Issues
class BuildService < Issues::BaseService
+ include ResolveDiscussions
+
def execute
+ filter_resolve_discussion_params
@issue = project.issues.new(issue_params)
end
- def issue_params_with_info_from_merge_request
- return {} unless merge_request_for_resolving_discussions
+ def issue_params_with_info_from_discussions
+ return {} unless merge_request_to_resolve_discussions_of
- { title: title_from_merge_request, description: description_from_merge_request }
+ { title: title_from_merge_request, description: description_for_discussions }
end
def title_from_merge_request
- "Follow-up from \"#{merge_request_for_resolving_discussions.title}\""
+ "Follow-up from \"#{merge_request_to_resolve_discussions_of.title}\""
end
- def description_from_merge_request
- if merge_request_for_resolving_discussions.resolvable_discussions.empty?
+ def description_for_discussions
+ if discussions_to_resolve.empty?
return "There are no unresolved discussions. "\
- "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}"
+ "Review the conversation in #{merge_request_to_resolve_discussions_of.to_reference}"
end
- description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:"
+ description = "The following #{'discussion'.pluralize(discussions_to_resolve.size)} "\
+ "from #{merge_request_to_resolve_discussions_of.to_reference} "\
+ "should be addressed:"
+
[description, *items_for_discussions].join("\n\n")
end
def items_for_discussions
- merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) }
+ discussions_to_resolve.map { |discussion| item_for_discussion(discussion) }
end
def item_for_discussion(discussion)
- first_note = discussion.first_note_to_resolve
+ first_note = discussion.first_note_to_resolve || discussion.first_note
other_note_count = discussion.notes.size - 1
- creation_time = first_note.created_at.to_s(:medium)
note_url = Gitlab::UrlBuilder.build(first_note)
- discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): "
+ discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): "
discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
- quote = ">>>\n#{note_without_block_quotes}\n>>>"
+ spaces = ' ' * 4
+ quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join
[discussion_info, quote].join("\n\n")
end
def issue_params
- @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
+ @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
end
def whitelisted_issue_params
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 366b3572738..3cf4b82b9f2 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,18 +1,20 @@
module Issues
class CreateService < Issues::BaseService
include SpamCheckService
+ include ResolveDiscussions
def execute
- filter_spam_check_params
+ @issue = BuildService.new(project, current_user, params).execute
- issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
- @issue = BuildService.new(project, current_user, issue_attributes).execute
+ filter_spam_check_params
+ filter_resolve_discussion_params
create(@issue)
end
def before_create(issue)
spam_check(issue, current_user)
+ issue.move_to_end
end
def after_create(issuable)
@@ -20,17 +22,16 @@ module Issues
notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
-
- if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
- resolve_discussions_in_merge_request(issuable)
- end
+ resolve_discussions_with_issue(issuable)
end
- def resolve_discussions_in_merge_request(issue)
+ def resolve_discussions_with_issue(issue)
+ return if discussions_to_resolve.empty?
+
Discussions::ResolveService.new(project, current_user,
- merge_request: merge_request_for_resolving_discussions,
+ merge_request: merge_request_to_resolve_discussions_of,
follow_up_issue: issue).
- execute(merge_request_for_resolving_discussions.resolvable_discussions)
+ execute(discussions_to_resolve)
end
private
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index a2a5f57d069..711f4035c55 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -1,6 +1,6 @@
module Issues
class MoveService < Issues::BaseService
- class MoveError < StandardError; end
+ MoveError = Class.new(StandardError)
def execute(issue, new_project)
@old_issue = issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 22e32b13259..a444c78b609 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -3,8 +3,8 @@ module Issues
include SpamCheckService
def execute(issue)
+ handle_move_between_iids(issue)
filter_spam_check_params
-
update(issue)
end
@@ -37,11 +37,13 @@ module Issues
end
added_labels = issue.labels - old_labels
+
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
added_mentions = issue.mentioned_users - old_mentioned_users
+
if added_mentions.present?
notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
end
@@ -55,8 +57,24 @@ module Issues
Issues::CloseService
end
+ def handle_move_between_iids(issue)
+ return unless params[:move_between_iids]
+
+ after_iid, before_iid = params.delete(:move_between_iids)
+
+ issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
+ issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
+
+ issue.move_between(issue_before, issue_after)
+ end
+
private
+ def get_issue_if_allowed(project, iid)
+ issue = project.issues.find_by(iid: iid)
+ issue if can?(current_user, :update_issue, issue)
+ end
+
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
diff --git a/app/services/mattermost/create_team_service.rb b/app/services/mattermost/create_team_service.rb
new file mode 100644
index 00000000000..e3206810f3a
--- /dev/null
+++ b/app/services/mattermost/create_team_service.rb
@@ -0,0 +1,14 @@
+module Mattermost
+ class CreateTeamService < ::BaseService
+ def initialize(group, current_user)
+ @group, @current_user = group, current_user
+ end
+
+ def execute
+ # The user that creates the team will be Team Admin
+ Mattermost::Team.new(current_user).create(@group.mattermost_team_params)
+ rescue Mattermost::ClientError => e
+ @group.errors.add(:mattermost_team, e.message)
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 431da8372c9..2e089149ca8 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -4,7 +4,7 @@ module Members
attr_accessor :source
- ALLOWED_SCOPES = %i[members requesters all]
+ ALLOWED_SCOPES = %i[members requesters all].freeze
def initialize(source, current_user, params = {})
@source = source
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bb..fdce542bd9e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
- merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
+ merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 1262ecbc29a..f00a33969a8 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute(changes)
+ return [] unless project.printing_merge_request_link_enabled
+
branches = get_branches(changes)
merge_requests_map = opened_merge_requests_from_source_branches(branches)
branches.map do |branch|
@@ -23,10 +25,7 @@ module MergeRequests
def opened_merge_requests_from_source_branches(branches)
merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
- merge_requests.inject({}) do |hash, mr|
- hash[mr.source_branch] = mr
- hash
- end
+ merge_requests.index_by(&:source_branch)
end
def get_branches(changes)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 3da1b657a41..fac3ac7a4c7 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,6 +6,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
+ MergeError = Class.new(StandardError)
+
attr_reader :merge_request, :source
def execute(merge_request)
@@ -27,6 +29,8 @@ module MergeRequests
success
end
end
+ rescue MergeError => e
+ log_merge_error(e.message, save_message_on_model: true)
end
private
@@ -42,19 +46,13 @@ module MergeRequests
commit_id = repository.merge(current_user, source, merge_request, options)
- if commit_id
- merge_request.update(merge_commit_sha: commit_id)
- else
- log_merge_error('Conflicts detected during merge', save_message_on_model: true)
- false
- end
+ raise MergeError, 'Conflicts detected during merge' unless commit_id
+
+ merge_request.update(merge_commit_sha: commit_id)
rescue GitHooksService::PreReceiveError => e
- log_merge_error(e.message, save_message_on_model: true)
- false
+ raise MergeError, e.message
rescue StandardError => e
- merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
- log_merge_error(e.message)
- false
+ raise MergeError, "Something went wrong during merge: #{e.message}"
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index 5616edf8b4a..aed5287940e 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -1,18 +1,18 @@
module MergeRequests
class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
- # Marks the passed `merge_request` to be merged when the build succeeds or
+ # Marks the passed `merge_request` to be merged when the pipeline succeeds or
# updates the params for the automatic merge
def execute(merge_request)
merge_request.merge_params.merge!(params)
# The service is also called when the merge params are updated.
- already_approved = merge_request.merge_when_build_succeeds?
+ already_approved = merge_request.merge_when_pipeline_succeeds?
unless already_approved
- merge_request.merge_when_build_succeeds = true
- merge_request.merge_user = @current_user
+ merge_request.merge_when_pipeline_succeeds = true
+ merge_request.merge_user = @current_user
- SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
+ SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
end
merge_request.save
@@ -23,8 +23,12 @@ module MergeRequests
return unless pipeline.success?
pipeline_merge_requests(pipeline) do |merge_request|
- next unless merge_request.merge_when_build_succeeds?
- next unless merge_request.mergeable?
+ next unless merge_request.merge_when_pipeline_succeeds?
+
+ unless merge_request.mergeable?
+ todo_service.merge_request_became_unmergeable(merge_request)
+ next
+ end
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
@@ -32,9 +36,9 @@ module MergeRequests
# Cancels the automatic merge
def cancel(merge_request)
- if merge_request.merge_when_build_succeeds? && merge_request.open?
- merge_request.reset_merge_when_build_succeeds
- SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
+ if merge_request.merge_when_pipeline_succeeds? && merge_request.open?
+ merge_request.reset_merge_when_pipeline_succeeds
+ SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
success
else
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 581d18032e6..1131d6f4913 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -11,7 +11,7 @@ module MergeRequests
# empty diff during a manual merge
close_merge_requests
reload_merge_requests
- reset_merge_when_build_succeeds
+ reset_merge_when_pipeline_succeeds
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -78,8 +78,8 @@ module MergeRequests
end
end
- def reset_merge_when_build_succeeds
- merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
+ def reset_merge_when_pipeline_succeeds
+ merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
end
def mark_pending_todos_done
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index d22a1d3e0ad..82cd89d9a0b 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,7 +1,6 @@
module MergeRequests
class ResolveService < MergeRequests::BaseService
- class MissingFiles < Gitlab::Conflict::ResolutionError
- end
+ MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
attr_accessor :conflicts, :rugged, :merge_index, :merge_request
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index b4f8b33d564..61d66a26932 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,14 +8,6 @@ module Notes
note.author = current_user
note.system = false
- if note.award_emoji?
- noteable = note.noteable
- if noteable.user_can_award?(current_user, note.award_emoji_name)
- todo_service.new_award_emoji(noteable, current_user)
- return noteable.create_award_emoji(note.award_emoji_name, current_user)
- end
- end
-
# We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
# only, there is no need be create a note!
@@ -48,7 +40,7 @@ module Notes
note.errors.add(:commands_only, 'Commands applied')
end
- note.commands_changes = command_params.keys
+ note.commands_changes = command_params
end
note
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 56913568cae..ad1e6f6774a 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -3,7 +3,7 @@ module Notes
UPDATE_SERVICES = {
'Issue' => Issues::UpdateService,
'MergeRequest' => MergeRequests::UpdateService
- }
+ }.freeze
def self.noteable_update_service(note)
UPDATE_SERVICES[note.noteable_type]
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 00000000000..44ae23fad18
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+ custom_action = build_custom_key(action, target)
+
+ recipients = target.participants(current_user)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients)
+ end
+
+ recipients = add_custom_notifications(recipients, custom_action)
+ recipients = reject_mention_users(recipients)
+
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients)
+ recipients = add_subscribed_users(recipients, target)
+
+ if [:new_issue, :new_merge_request].include?(custom_action)
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+
+ recipients.delete(current_user) if skip_current_user
+
+ recipients.uniq
+ end
+
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+ recipients.delete(current_user)
+ recipients.uniq
+ end
+
+ def build_new_note_recipients(note)
+ target = note.noteable
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
+ mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+ # Add all users participating in the thread (author, assignee, comment authors)
+ recipients =
+ if target.respond_to?(:participants)
+ target.participants(note.author)
+ else
+ mentioned_users
+ end
+
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients)
+
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, :new_note)
+ end
+
+ # Reject users with Mention notification level, except those mentioned in _this_ note.
+ recipients = reject_mention_users(recipients - mentioned_users)
+ recipients = recipients + mentioned_users
+
+ recipients = reject_muted_users(recipients)
+
+ recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
+
+ recipients.delete(note.author)
+ recipients.uniq
+ end
+
+ # Remove users with disabled notifications from array
+ # Also remove duplications and nil recipients
+ def reject_muted_users(users)
+ reject_users(users, :disabled)
+ end
+
+ protected
+
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += user_ids_notifiable_on(project, :custom, action)
+ user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+ # Users with global level custom
+ user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+
+ global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+ user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
+ def add_project_watchers(recipients)
+ recipients.concat(project_watchers).compact
+ end
+
+ # Get project users with WATCH notification level
+ def project_watchers
+ project_members_ids = user_ids_notifiable_on(project)
+
+ user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
+
+ user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+ user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+ user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+ User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+ end
+
+ # Remove users with notification level 'Mentioned'
+ def reject_mention_users(users)
+ reject_users(users, :mention)
+ end
+
+ def add_subscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers(project)
+ end
+
+ def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
+ if notification_level
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
+ else
+ resource.notification_settings.pluck(:user_id)
+ end
+ end
+
+ # Build a list of user_ids based on project notification settings
+ def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+ user_ids = user_ids_notifiable_on(project, :watch)
+
+ # If project setting is global, add to watch list if global setting is watch
+ global_setting.each do |user_id|
+ if user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ # Build a list of user_ids based on group notification settings
+ def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+ uids = user_ids_notifiable_on(group, :watch)
+
+ # Group setting is watch, add to user_ids list if user is not project member
+ user_ids = []
+ uids.each do |user_id|
+ if project_members.exclude?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ # Group setting is global, add to user_ids list if global setting is watch
+ global_setting.each do |user_id|
+ if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ def user_ids_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
+ end
+
+ def user_ids_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
+ end
+
+ # Reject users which has certain notification level
+ #
+ # Example:
+ # reject_users(users, :watch, project)
+ #
+ def reject_users(users, level)
+ level = level.to_s
+
+ unless NotificationSetting.levels.keys.include?(level)
+ raise 'Invalid notification level'
+ end
+
+ users = users.to_a.compact.uniq
+ users = users.select { |u| u.can?(:receive_notifications) }
+
+ users.reject do |user|
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
+
+ setting = user.notification_settings_for(project)
+
+ if project.group && (setting.nil? || setting.global?)
+ setting = user.notification_settings_for(project.group)
+ end
+
+ # reject users who globally set mention notification and has no setting per project/group
+ next global_notification_setting.level == level unless setting
+
+ # reject users who set mention notification in project
+ next true if setting.level == level
+
+ # reject users who have mention level in project and disabled in global settings
+ setting.global? && global_notification_setting.level == level
+ end
+ end
+
+ def reject_unsubscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ recipients.reject do |user|
+ subscription = target.subscriptions.find_by_user_id(user.id)
+ subscription && !subscription.subscribed
+ end
+ end
+
+ def reject_users_without_access(recipients, target)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
+
+ return recipients unless ability
+
+ recipients.select do |user|
+ user.can?(ability, target)
+ end
+ end
+
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers(project)
+ end
+
+ recipients
+ end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3734e3c4253..f9aa2346759 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -135,7 +135,7 @@ class NotificationService
merge_request.target_project,
current_user,
:merged_merge_request_email,
- skip_current_user: !merge_request.merge_when_build_succeeds?
+ skip_current_user: !merge_request.merge_when_pipeline_succeeds?
)
end
@@ -150,7 +150,10 @@ class NotificationService
end
def resolve_all_discussions(merge_request, current_user)
- recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+ recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+ merge_request,
+ current_user,
+ action: "resolve_all_discussions")
recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
end
# Notify users on new note in system
- #
- # TODO: split on methods and refactor
- #
def new_note(note)
return true unless note.noteable_type.present?
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
- target = note.noteable
-
- recipients = []
-
- mentioned_users = note.mentioned_users
-
- ability, subject = if note.for_personal_snippet?
- [:read_personal_snippet, note.noteable]
- else
- [:read_project, note.project]
- end
-
- mentioned_users.select! do |user|
- user.can?(ability, subject)
- end
-
- # Add all users participating in the thread (author, assignee, comment authors)
- participants =
- if target.respond_to?(:participants)
- target.participants(note.author)
- else
- mentioned_users
- end
-
- recipients = recipients.concat(participants)
-
- unless note.for_personal_snippet?
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
-
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
- end
-
- # Reject users with Mention notification level, except those mentioned in _this_ note.
- recipients = reject_mention_users(recipients - mentioned_users, note.project)
- recipients = recipients + mentioned_users
-
- recipients = reject_muted_users(recipients, note.project)
-
- recipients = add_subscribed_users(recipients, note.project, note.noteable)
- recipients = reject_unsubscribed_users(recipients, note.noteable)
- recipients = reject_users_without_access(recipients, note.noteable)
-
- recipients.delete(note.author) unless note.author.notified_of_own_activity?
- recipients = recipients.uniq
-
notify_method = "note_#{note.to_ability_name}_email".to_sym
+ recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -290,7 +244,7 @@ class NotificationService
def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
- recipients = reject_muted_users(recipients, project)
+ recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
end
def issue_moved(issue, new_issue, current_user)
- recipients = build_recipients(issue, issue.project, current_user)
+ recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,12 +278,10 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= build_recipients(
+ recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline,
- pipeline.project,
- pipeline.user,
- action: pipeline.status,
- skip_current_user: false).map(&:notification_email)
+ nil, # The acting user, who won't be added to recipients
+ action: pipeline.status).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
@@ -338,199 +290,8 @@ class NotificationService
protected
- # Get project/group users with CUSTOM notification level
- def add_custom_notifications(recipients, project, action)
- user_ids = []
-
- # Users with a notification setting on group or project
- user_ids += notification_settings_for(project, :custom, action)
- user_ids += notification_settings_for(project.group, :custom, action)
-
- # Users with global level custom
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
- user_ids += users_with_global_level_custom(global_users_ids, action)
-
- recipients.concat(User.find(user_ids))
- end
-
- # Get project users with WATCH notification level
- def project_watchers(project)
- project_members = notification_settings_for(project)
-
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
- users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
- User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
- end
-
- def notification_settings_for(resource, notification_level = nil, action = nil)
- return [] unless resource
-
- if notification_level
- settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
- settings = settings.select { |setting| setting.events[action] } if action.present?
- settings.map(&:user_id)
- else
- resource.notification_settings.pluck(:user_id)
- end
- end
-
- def users_with_global_level_watch(ids)
- settings_with_global_level_of(:watch, ids).pluck(:user_id)
- end
-
- def users_with_global_level_custom(ids, action)
- settings = settings_with_global_level_of(:custom, ids)
- settings = settings.select { |setting| setting.events[action] }
- settings.map(&:user_id)
- end
-
- def settings_with_global_level_of(level, ids)
- NotificationSetting.where(
- user_id: ids,
- source_type: nil,
- level: NotificationSetting.levels[level]
- )
- end
-
- # Build a list of users based on project notification settings
- def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = notification_settings_for(project, :watch)
-
- # If project setting is global, add to watch list if global setting is watch
- global_setting.each do |user_id|
- if users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- # Build a list of users based on group notification settings
- def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(group, :watch)
-
- # Group setting is watch, add to users list if user is not project member
- users = []
- uids.each do |user_id|
- if project_members.exclude?(user_id)
- users << user_id
- end
- end
-
- # Group setting is global, add to users list if global setting is watch
- global_setting.each do |user_id|
- if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact
- end
-
- # Remove users with disabled notifications from array
- # Also remove duplications and nil recipients
- def reject_muted_users(users, project = nil)
- reject_users(users, :disabled, project)
- end
-
- # Remove users with notification level 'Mentioned'
- def reject_mention_users(users, project = nil)
- reject_users(users, :mention, project)
- end
-
- # Reject users which has certain notification level
- #
- # Example:
- # reject_users(users, :watch, project)
- #
- def reject_users(users, level, project = nil)
- level = level.to_s
-
- unless NotificationSetting.levels.keys.include?(level)
- raise 'Invalid notification level'
- end
-
- users = users.to_a.compact.uniq
- users = users.reject(&:blocked?)
-
- users.reject do |user|
- global_notification_setting = user.global_notification_setting
-
- next global_notification_setting.level == level unless project
-
- setting = user.notification_settings_for(project)
-
- if project.group && (setting.nil? || setting.global?)
- setting = user.notification_settings_for(project.group)
- end
-
- # reject users who globally set mention notification and has no setting per project/group
- next global_notification_setting.level == level unless setting
-
- # reject users who set mention notification in project
- next true if setting.level == level
-
- # reject users who have mention level in project and disabled in global settings
- setting.global? && global_notification_setting.level == level
- end
- end
-
- def reject_unsubscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
-
- recipients.reject do |user|
- subscription = target.subscriptions.find_by_user_id(user.id)
- subscription && !subscription.subscribed
- end
- end
-
- def reject_users_without_access(recipients, target)
- ability = case target
- when Issuable
- :"read_#{target.to_ability_name}"
- when Ci::Pipeline
- :read_build # We have build trace in pipeline emails
- end
-
- return recipients unless ability
-
- recipients.select do |user|
- user.can?(ability, target)
- end
- end
-
- def add_subscribed_users(recipients, project, target)
- return recipients unless target.respond_to? :subscribers
-
- recipients + target.subscribers(project)
- end
-
- def add_labels_subscribers(recipients, project, target, labels: nil)
- return recipients unless target.respond_to? :labels
-
- (labels || target.labels).each do |label|
- recipients += label.subscribers(project)
- end
-
- recipients
- end
-
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -538,7 +299,7 @@ class NotificationService
end
def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
- recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users
recipients.each do |recipient|
@@ -549,9 +310,8 @@ class NotificationService
def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(
+ recipients = NotificationRecipientService.new(project).build_recipients(
target,
- project,
current_user,
action: action,
skip_current_user: skip_current_user
@@ -566,7 +326,12 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+ recipients = NotificationRecipientService.new(project).build_recipients(
+ target,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
recipients.each do |recipient|
mailer.send(
@@ -580,7 +345,7 @@ class NotificationService
end
def relabeled_resource_email(target, project, labels, current_user, method)
- recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+ recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -589,58 +354,13 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user, action: "reopen")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
- custom_action = build_custom_key(action, target)
-
- recipients = target.participants(current_user)
-
- unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
- recipients = add_project_watchers(recipients, project)
- end
-
- recipients = add_custom_notifications(recipients, project, custom_action)
- recipients = reject_mention_users(recipients, project)
-
- recipients = recipients.uniq
-
- # Re-assign is considered as a mention of the new assignee so we add the
- # new assignee to the list of recipients after we rejected users with
- # the "on mention" notification level
- if [:reassign_merge_request, :reassign_issue].include?(custom_action)
- recipients << previous_assignee if previous_assignee
- recipients << target.assignee
- end
-
- recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, project, target)
-
- if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, project, target)
- end
-
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
-
- recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
-
- recipients.uniq
- end
-
- def build_relabeled_recipients(target, project, current_user, labels:)
- recipients = add_labels_subscribers([], project, target, labels: labels)
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) unless current_user.notified_of_own_activity?
- recipients.uniq
- end
-
def mailer
Notify
end
@@ -652,10 +372,4 @@ class NotificationService
end
end
end
-
- # Build event key to search on custom notification level
- # Check NotificationSetting::EMAIL_EVENTS
- def build_custom_key(action, object)
- "#{action}_#{object.class.model_name.name.underscore}".to_sym
- end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 6dc3d8c2416..fbdaa455651 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -12,7 +12,7 @@ module Projects
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level)
deny_visibility_level(@project)
return @project
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index ba410b79e8c..4e1964f79dd 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -2,9 +2,9 @@ module Projects
class DestroyService < BaseService
include Gitlab::ShellAdapter
- class DestroyError < StandardError; end
+ DestroyError = Class.new(StandardError)
- DELETED_FLAG = '+deleted'
+ DELETED_FLAG = '+deleted'.freeze
def async_execute
project.transaction do
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index f06a3d44c17..604747e39d0 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -2,7 +2,7 @@ module Projects
class DownloadService < BaseService
WHITELIST = [
/^[^.]+\.fogbugz.com$/
- ]
+ ].freeze
def initialize(project, url)
@project, @url = project, url
@@ -25,7 +25,7 @@ module Projects
end
def http?(url)
- url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+ url =~ /\A#{URI.regexp(%w(http https))}\z/
end
def valid_domain?(url)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cd230528743..d484a96f785 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -2,7 +2,7 @@ module Projects
class ImportService < BaseService
include Gitlab::ShellAdapter
- class Error < StandardError; end
+ Error = Class.new(StandardError)
def execute
add_repository_to_project unless project.gitlab_project_import?
@@ -33,6 +33,7 @@ module Projects
def import_repository
begin
+ raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
# Expire cache to prevent scenarios such as:
@@ -40,7 +41,7 @@ module Projects
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.before_import if project.repository_exists?
- raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+ raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 3e241b9e7c0..6d9e7de4f24 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -9,7 +9,7 @@
module Projects
class TransferService < BaseService
include Gitlab::ShellAdapter
- class TransferError < StandardError; end
+ TransferError = Class.new(StandardError)
def execute(new_namespace)
if allowed_transfer?(current_user, project, new_namespace)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index f5f9ee88912..523b9f41916 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -2,7 +2,7 @@ module Projects
class UpdatePagesService < BaseService
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
- SITE_PATH = 'public/'
+ SITE_PATH = 'public/'.freeze
attr_reader :build
@@ -34,6 +34,8 @@ module Projects
end
rescue => e
error(e.message)
+ ensure
+ build.erase_artifacts! unless build.has_expiring_artifacts?
end
private
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
index 050cb3b738b..bdb0e0cc8bf 100644
--- a/app/services/protected_branches/api_update_service.rb
+++ b/app/services/protected_branches/api_update_service.rb
@@ -15,16 +15,16 @@ module ProtectedBranches
case @developers_can_push
when true
- params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
- params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
end
case @developers_can_merge
when true
- params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
- params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
end
service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 3e0a85cf059..595653ea58a 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -59,7 +59,7 @@ module SlashCommands
@updates[:state_event] = 'reopen'
end
- desc 'Merge (when build succeeds)'
+ desc 'Merge (when the pipeline succeeds)'
condition do
last_diff_sha = params && params[:merge_request_diff_head_sha]
issuable.is_a?(MergeRequest) &&
@@ -255,6 +255,18 @@ module SlashCommands
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end
+ desc 'Toggle emoji reward'
+ params ':emoji:'
+ condition do
+ issuable.persisted?
+ end
+ command :award do |emoji|
+ name = award_emoji_name(emoji)
+ if name && issuable.user_can_award?(current_user, name)
+ @updates[:emoji_award] = name
+ end
+ end
+
desc 'Set time estimate'
params '<1w 3d 2h 14m>'
condition do
@@ -329,5 +341,10 @@ module SlashCommands
ext.references(type)
end
+
+ def award_emoji_name(emoji)
+ match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)
+ match[1] if match
+ end
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a2bfa422c9d..af0ddbe5934 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -24,21 +24,16 @@ class SystemHooksService
key: model.key,
id: model.id
)
+
if model.user
- data.merge!(
- username: model.user.username
- )
+ data[:username] = model.user.username
end
when Project
data.merge!(project_data(model))
if event == :rename || event == :transfer
- data.merge!({
- old_path_with_namespace: model.old_path_with_namespace
- })
+ data[:old_path_with_namespace] = model.old_path_with_namespace
end
-
- data
when User
data.merge!({
name: model.name,
@@ -61,6 +56,8 @@ class SystemHooksService
when GroupMember
data.merge!(group_member_data(model))
end
+
+ data
end
def build_event_name(model, event)
@@ -86,7 +83,7 @@ class SystemHooksService
project_id: model.id,
owner_name: owner.name,
owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
+ project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase
}
end
@@ -103,7 +100,7 @@ class SystemHooksService
user_email: model.user.email,
user_id: model.user.id,
access_level: model.human_access,
- project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase
+ project_visibility: Project.visibility_levels.key(project.visibility_level_value).downcase
}
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 87ba72cf991..8e02fe3741a 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -187,14 +187,14 @@ module SystemNoteService
end
# Called when 'merge when pipeline succeeds' is executed
- def merge_when_build_succeeds(noteable, project, author, last_commit)
+ def merge_when_pipeline_succeeds(noteable, project, author, last_commit)
body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when 'merge when pipeline succeeds' is canceled
- def cancel_merge_when_build_succeeds(noteable, project, author)
+ def cancel_merge_when_pipeline_succeeds(noteable, project, author)
body = 'canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -356,10 +356,10 @@ module SystemNoteService
note: cross_reference_note_content(gfm_reference)
}
- if noteable.kind_of?(Commit)
+ if noteable.is_a?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
- note_options.merge!(noteable: noteable)
+ note_options[:noteable] = noteable
end
if noteable.is_a?(ExternalIssue)
@@ -385,7 +385,6 @@ module SystemNoteService
# Returns Boolean
def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
- return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
@@ -408,12 +407,13 @@ module SystemNoteService
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
- if noteable.is_a?(Commit)
- # Commits have non-integer IDs, so they're stored in `commit_id`
- notes = notes.where(commit_id: noteable.id)
- else
- notes = notes.where(noteable_id: noteable.id)
- end
+ notes =
+ if noteable.is_a?(Commit)
+ # Commits have non-integer IDs, so they're stored in `commit_id`
+ notes.where(commit_id: noteable.id)
+ else
+ notes.where(noteable_id: noteable.id)
+ end
notes_for_mentioner(mentioner, noteable, notes).exists?
end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 910b4f5e361..a368f4f5b61 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,6 +21,8 @@ module Tags
else
error('Failed to remove tag')
end
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
end
def error(message, return_code = 400)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index ad86b4f9f42..bf7e76ec59e 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -103,7 +103,7 @@ class TodoService
#
def merge_request_build_failed(merge_request)
create_build_failed_todo(merge_request, merge_request.author)
- create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
# When a new commit is pushed to a merge request we should:
@@ -121,7 +121,7 @@ class TodoService
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
- mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
# When a merge request could not be automatically merged due to its unmergeable state we should:
@@ -129,7 +129,7 @@ class TodoService
# * create a todo for a merge_user
#
def merge_request_became_unmergeable(merge_request)
- create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
# When create a note we should:
@@ -201,10 +201,12 @@ class TodoService
def update_todos_state_by_ids(ids, current_user, state)
todos = current_user.todos.where(id: ids)
- # Only return those that are not really on that state
- marked_todos = todos.where.not(state: state).update_all(state: state)
+ # Only update those that are not really on that state
+ todos = todos.where.not(state: state)
+ todos_ids = todos.pluck(:id)
+ todos.update_all(state: state)
current_user.update_todos_count_cache
- marked_todos
+ todos_ids
end
def create_todos(users, attributes)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index bc0653cb634..833da5bc5d1 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -7,7 +7,7 @@ module Users
end
def execute(user, options = {})
- unless current_user.admin? || current_user == user
+ unless Ability.allowed?(current_user, :destroy_user, user)
raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
end
@@ -26,6 +26,8 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end
+ move_issues_to_ghost_user(user)
+
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
user_data = user.destroy
@@ -33,5 +35,22 @@ module Users
user_data
end
+
+ private
+
+ def move_issues_to_ghost_user(user)
+ # Block the user before moving issues to prevent a data race.
+ # If the user creates an issue after `move_issues_to_ghost_user`
+ # runs and before the user is destroyed, the destroy will fail with
+ # an exception. We block the user so that issues can't be created
+ # after `move_issues_to_ghost_user` runs and before the destroy happens.
+ user.block
+
+ ghost_user = User.ghost
+
+ user.issues.update_all(author_id: ghost_user.id)
+
+ user.reload
+ end
end
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index fad741531ea..8f6f5b937c4 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -93,9 +93,7 @@ module Users
end
def current_authorizations_per_project
- current_authorizations.each_with_object({}) do |row, hash|
- hash[row.project_id] = row
- end
+ current_authorizations.index_by(&:project_id)
end
def current_authorizations
@@ -115,11 +113,23 @@ module Users
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
+ # Personal projects
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
- user.groups_projects.select_for_project_authorization,
+
+ # Projects the user is a member of
user.projects.select_for_project_authorization,
+
+ # Projects of groups the user is a member of
+ user.groups_projects.select_for_project_authorization,
+
+ # Projects of subgroups of groups the user is a member of
+ user.nested_groups_projects.select_for_project_authorization,
+
+ # Projects shared with groups the user is a member of
user.groups.joins(:shared_projects).select_for_project_authorization,
- user.nested_projects.select_for_project_authorization
+
+ # Projects shared with subgroups of groups the user is a member of
+ user.nested_groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 86f317dcd18..e84944ed411 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader
File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
def filename
file.try(:filename)
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index cfcb877cc3e..109eb2fea0b 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,9 +1,10 @@
class AttachmentUploader < GitlabUploader
+ include RecordsUploads
include UploaderHelper
storage :file
def store_dir
- "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 265cea2d2c6..66d3bcb998a 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,10 +1,11 @@
class AvatarUploader < GitlabUploader
+ include RecordsUploads
include UploaderHelper
storage :file
def store_dir
- "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def exists?
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 23b7318827c..d6ccf0dc92c 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,30 +1,53 @@
class FileUploader < GitlabUploader
+ include RecordsUploads
include UploaderHelper
+
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file
- attr_accessor :project, :secret
+ def self.absolute_path(upload_record)
+ File.join(
+ self.dynamic_path_segment(upload_record.model),
+ upload_record.path
+ )
+ end
- def initialize(project, secret = nil)
- @project = project
- @secret = secret || self.class.generate_secret
+ # Returns the part of `store_dir` that can change based on the model's current
+ # path
+ #
+ # This is used to build Upload paths dynamically based on the model's current
+ # namespace and path, allowing us to ignore renames or transfers.
+ #
+ # model - Object that responds to `path_with_namespace`
+ #
+ # Returns a String without a trailing slash
+ def self.dynamic_path_segment(model)
+ File.join(CarrierWave.root, base_dir, model.path_with_namespace)
end
- def base_dir
- "uploads"
+ attr_accessor :project
+ attr_reader :secret
+
+ def initialize(project, secret = nil)
+ @project = project
+ @secret = secret || generate_secret
end
def store_dir
- File.join(base_dir, @project.path_with_namespace, @secret)
+ File.join(dynamic_path_segment, @secret)
end
def cache_dir
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
- def secure_url
- File.join("/uploads", @secret, file.filename)
+ def model
+ project
+ end
+
+ def relative_path
+ self.file.path.sub("#{dynamic_path_segment}/", '')
end
def to_markdown
@@ -35,17 +58,27 @@ class FileUploader < GitlabUploader
filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
- markdown = "[#{escaped_filename}](#{self.secure_url})"
+ markdown = "[#{escaped_filename}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
{
alt: filename,
- url: self.secure_url,
+ url: secure_url,
markdown: markdown
}
end
- def self.generate_secret
+ private
+
+ def dynamic_path_segment
+ self.class.dynamic_path_segment(model)
+ end
+
+ def generate_secret
SecureRandom.hex
end
+
+ def secure_url
+ File.join('/uploads', @secret, file.filename)
+ end
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 02d7c601d6c..d662ba6820c 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,4 +1,18 @@
class GitlabUploader < CarrierWave::Uploader::Base
+ def self.absolute_path(upload_record)
+ File.join(CarrierWave.root, upload_record.path)
+ end
+
+ def self.base_dir
+ 'uploads'
+ end
+
+ delegate :base_dir, to: :class
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+
# Reduce disk IO
def move_to_cache
true
@@ -8,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base
def move_to_store
true
end
+
+ # Designed to be overridden by child uploaders that have a dynamic path
+ # segment -- that is, a path that changes based on mutable attributes of its
+ # associated model
+ #
+ # For example, `FileUploader` builds the storage path based on the associated
+ # project model's `path_with_namespace` value, which can change when the
+ # project or its containing namespace is moved or renamed.
+ def relative_path
+ self.file.path.sub("#{root}/", '')
+ end
end
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
new file mode 100644
index 00000000000..4c127f29250
--- /dev/null
+++ b/app/uploaders/records_uploads.rb
@@ -0,0 +1,34 @@
+module RecordsUploads
+ extend ActiveSupport::Concern
+
+ included do
+ after :store, :record_upload
+ before :remove, :destroy_upload
+ end
+
+ private
+
+ # After storing an attachment, create a corresponding Upload record
+ #
+ # NOTE: We're ignoring the argument passed to this callback because we want
+ # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
+ # `Tempfile` object the callback gets.
+ #
+ # Called `after :store`
+ def record_upload(_tempfile)
+ return unless file_storage?
+ return unless file.exists?
+
+ Upload.record(self)
+ end
+
+ # Before removing an attachment, destroy any Upload records at the same path
+ #
+ # Called `before :remove`
+ def destroy_upload(*args)
+ return unless file_storage?
+ return unless file
+
+ Upload.remove_path(relative_path)
+ end
+end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 35fd1ed23f8..7635c20ab3a 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,15 +1,15 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
- VIDEO_EXT = %w[mp4 m4v mov webm ogv]
+ VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
# These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
- DANGEROUS_EXT = %w[svg]
+ DANGEROUS_EXT = %w[svg].freeze
def image?
extension_match?(IMAGE_EXT)
@@ -27,6 +27,8 @@ module UploaderHelper
extension_match?(DANGEROUS_EXT)
end
+ private
+
def extension_match?(extensions)
return false unless file
@@ -40,8 +42,4 @@ module UploaderHelper
extensions.include?(extension.downcase)
end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 09bfa613cbe..94542125d43 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -18,7 +18,7 @@
# end
#
class AddressableUrlValidator < ActiveModel::EachValidator
- DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }
+ DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze
def validate_each(record, attribute, value)
unless valid_url?(value)
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
new file mode 100644
index 00000000000..10ff44031c6
--- /dev/null
+++ b/app/validators/duration_validator.rb
@@ -0,0 +1,17 @@
+# DurationValidator
+#
+# Validate the format conforms with ChronicDuration
+#
+# Example:
+#
+# class ApplicationSetting < ActiveRecord::Base
+# validates :default_artifacts_expire_in, presence: true, duration: true
+# end
+#
+class DurationValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ record.errors.add(attribute, "is not a correct duration")
+ end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
new file mode 100644
index 00000000000..37a314adee6
--- /dev/null
+++ b/app/validators/importable_url_validator.rb
@@ -0,0 +1,11 @@
+# ImportableUrlValidator
+#
+# This validator blocks projects from using dangerous import_urls to help
+# protect against Server-side Request Forgery (SSRF).
+class ImportableUrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if Gitlab::UrlBlocker.blocked_url?(value)
+ record.errors.add(attribute, "imports are not allowed from that URL")
+ end
+ end
+end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index eb3ed31b65b..77ca033e97f 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -35,12 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
users
].freeze
+ WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
+ preview blob blame raw files create_dir find_file
+ artifacts graphs refs badges].freeze
+
+ STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
+
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
- def self.reserved?(value)
- RESERVED.include?(value)
+ def self.reserved?(value, strict: false)
+ if strict
+ STRICT_RESERVED.include?(value)
+ else
+ RESERVED.include?(value)
+ end
end
def self.follow_format?(value)
@@ -54,7 +64,9 @@ class NamespaceValidator < ActiveModel::EachValidator
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
- if reserved?(value)
+ strict = record.is_a?(Group) && record.parent_id
+
+ if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 36279daa743..ee2ae65be7b 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
- RESERVED = (NamespaceValidator::RESERVED -
- %w[dashboard help ci admin search notes services assets profile public] +
- %w[tree commits wikis new edit create update logs_tree
- preview blob blame raw files create_dir find_file]).freeze
+ RESERVED = (NamespaceValidator::STRICT_RESERVED -
+ %w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 6c48328da4f..6a5e170ddd8 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -16,4 +16,4 @@
- else
.empty-state
.text-center
- %h4 There are no abuse reports! #{emoji_icon 'tada'}
+ %h4 There are no abuse reports! #{emoji_icon('tada')}
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 9175b3d3f96..e403a9da616 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -48,7 +48,7 @@
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
- = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.pull-right
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 749c74b8110..3eab065bb9f 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -212,8 +212,16 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
.help-block
- Set the maximum file size each jobs's artifacts can have
- = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
+ Set the maximum file size for each job's artifacts
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ .form-group
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :default_artifacts_expire_in, class: 'form-control'
+ .help-block
+ Set the default expiration time for each job's artifacts.
+ 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
- if Gitlab.config.registry.enabled
%fieldset
@@ -352,6 +360,29 @@
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :unique_ips_limit_enabled do
+ = f.check_box :unique_ips_limit_enabled
+ Limit sign in from multiple ips
+ %span.help-block#unique_ip_help_block
+ Helps prevent malicious users hide their activity
+
+ .form-group
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .help-block
+ Maximum number of unique IPs per user
+
+ .form-group
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .help-block
+ How many seconds an IP will be counted towards the limit
+
%fieldset
%legend Abuse reports
.form-group
@@ -373,7 +404,7 @@
Enable Sentry
.help-block
Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
.form-group
= f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index c689b26d6e6..061f8991b11 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -26,4 +26,4 @@
.form-actions
= f.submit 'Submit', class: "btn btn-save wide"
- = link_to "Cancel", admin_applications_path, class: "btn btn-default"
+ = link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
new file mode 100644
index 00000000000..1378dde52ab
--- /dev/null
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -0,0 +1,8 @@
+- page_title "Impersonation Tokens", @user.name, "Users"
+= render 'admin/users/head'
+
+.row.prepend-top-default
+ .col-lg-12
+ = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
+
+ = render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
new file mode 100644
index 00000000000..c1a9f8d6ddd
--- /dev/null
+++ b/app/views/admin/projects/_projects.html.haml
@@ -0,0 +1,32 @@
+.js-projects-list-holder
+ - if @projects.any?
+ %ul.projects-list.content-list
+ - @projects.each_with_index do |project|
+ %li.project-row
+ .controls
+ - if project.archived
+ %span.label.label-warning archived
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
+ = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
+ = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
+ .title
+ = link_to [:admin, project.namespace.becomes(Namespace), project] do
+ .dash-project-avatar
+ .avatar-container.s40
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name.filter-title
+ = project.name
+
+ - if project.description.present?
+ .description
+ = markdown_field(project, :description)
+
+ = paginate @projects, theme: 'gitlab'
+ - else
+ .nothing-here-block No projects found
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index c35945c5a35..3301f55b8a8 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,40 +7,24 @@
%div{ class: container_class }
.top-area
.prepend-top-default
- = form_tag admin_projects_path, method: :get do |f|
- .search-holder
- .search-field-holder
- = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
-
- - if params[:visibility_level].present?
- = hidden_field_tag 'visibility_level', params[:visibility_level]
-
- - if params[:sort].present?
- = hidden_field_tag 'sort', params[:sort]
-
- - if params[:personal].present?
- = hidden_field_tag 'visibility_level', 'true'
-
- - if params[:archived].present?
- = hidden_field_tag 'archived', 'true'
-
- = icon("search", class: "search-icon")
-
- .dropdown
- - toggle_text = 'Namespace'
- - if params[:namespace_id].present?
- - namespace = Namespace.find(params[:namespace_id])
- - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select.dropdown-menu-align-right
- = dropdown_title('Namespaces')
- = dropdown_filter("Search for Namespace")
- = dropdown_content
- = dropdown_loading
- = render 'shared/projects/dropdown'
- = link_to new_project_path, class: 'btn btn-new' do
- New Project
- = button_tag "Search", class: "btn btn-primary btn-search hide"
+ .search-holder
+ = render 'shared/projects/search_form', autofocus: true, icon: true
+ .dropdown
+ - toggle_text = 'Namespace'
+ - if params[:namespace_id].present?
+ = hidden_field_tag :namespace_id, params[:namespace_id]
+ - namespace = Namespace.find(params[:namespace_id])
+ - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right
+ = dropdown_title('Namespaces')
+ = dropdown_filter("Search for Namespace")
+ = dropdown_content
+ = dropdown_loading
+ = render 'shared/projects/dropdown'
+ = link_to new_project_path, class: 'btn btn-new' do
+ New Project
+ = button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
@@ -58,35 +42,4 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
- .projects-list-holder
- - if @projects.any?
- %ul.projects-list.content-list
- - @projects.each_with_index do |project|
- %li.project-row
- .controls
- - if project.archived
- %span.label.label-warning archived
- %span.badge
- = storage_counter(project.statistics.storage_size)
- = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
- = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
- .title
- = link_to [:admin, project.namespace.becomes(Namespace), project] do
- .dash-project-avatar
- .avatar-container.s40
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
- %span.project-full-name
- %span.namespace-name
- - if project.namespace
- = project.namespace.human_name
- \/
- %span.project-name.filter-title
- = project.name
-
- - if project.description.present?
- .description
- = markdown_field(project, :description)
-
- = paginate @projects, theme: 'gitlab'
- - else
- .nothing-here-block No projects found
+ = render 'projects'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index deb62845e1c..d4d166ab7b6 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -15,6 +15,8 @@
%td
= runner.description
%td
+ = runner.version
+ %td
- if runner.shared?
n/a
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index d725e477044..7d26864d0f3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -67,6 +67,7 @@
%th Type
%th Runner token
%th Description
+ %th Version
%th Projects
%th Jobs
%th Tags
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9984e733956..be41c33b853 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -2,11 +2,13 @@
= @user.name
- if @user.blocked?
%span.cred (Blocked)
+ - if @user.internal?
+ %span.cred (Internal)
- if @user.admin
%span.cred (Admin)
.pull-right
- - unless @user == current_user || @user.blocked?
+ - if @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
@@ -21,4 +23,6 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+ = nav_link(controller: :impersonation_tokens) do
+ = link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user)
.append-bottom-default
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 3b5c713ac2d..a756cb7243a 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -34,7 +34,7 @@
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
+ - if user.can_be_removed? && can?(current_user, :destroy_user, @user)
%li.divider
%li
= link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 76b1291fe10..840d843f069 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -173,7 +173,7 @@
.panel-heading
Remove user
.panel-body
- - if @user.can_be_removed?
+ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
%ul
%li All user content like authored issues, snippets, comments will be removed
@@ -189,3 +189,6 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
+ - else
+ %p
+ You don't have access to delete this user.
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index e3305e21e96..a1ef34dc588 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -4,7 +4,7 @@
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: (award_state_class(awards, current_user)),
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
- = emoji_icon(emoji, sprite: false)
+ = emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index dc76599b776..89d991abe54 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -2,10 +2,9 @@
= render "events/event_last_push", event: @last_push
.nav-block
- - if current_user
- .controls
- = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
- %i.fa.fa-rss
+ .controls
+ = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
+ %i.fa.fa-rss
= render 'shared/event_filter'
.content_list
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 23c145ebbb4..13eaba41f4c 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -6,7 +6,9 @@
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore groups' do
Explore Groups
- - if current_user.can_create_group?
- .nav-controls
+ .nav-controls
+ = render 'shared/groups/search_form'
+ = render 'shared/groups/dropdown'
+ - if current_user.can_create_group?
= link_to new_group_path, class: "btn btn-new" do
New Group
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 48b0fd504f4..600ee63a5c0 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -13,8 +13,7 @@
Explore projects
.nav-controls
- = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
+ = render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index aa57df14c23..190ad4b40a5 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,6 +1,5 @@
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+ = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
new file mode 100644
index 00000000000..6c3bf1a2b3b
--- /dev/null
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -0,0 +1,6 @@
+.js-groups-list-holder
+ %ul.content-list
+ - @group_members.each do |group_member|
+ = render 'shared/groups/group', group: group_member.group, group_member: group_member
+
+ = paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 1a679c51774..73ab2c95ff9 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -5,9 +5,4 @@
- if @group_members.empty?
= render 'empty_state'
- else
- %ul.content-list
- - @group_members.each do |group_member|
- - group = group_member.group
- = render 'shared/groups/group', group: group, group_member: group_member
-
- = paginate @group_members, theme: 'gitlab'
+ = render 'groups'
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index bdea1064096..06fb531b546 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 653052f7c54..10867140d4f 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,17 +1,13 @@
- page_title "Issues"
- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues")
+ = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- - if current_user
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
- = icon('rss')
- %span.icon-label
- Subscribe
+ = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
+ = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 917bfbd47e9..505b475f55b 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,11 +1,11 @@
-- page_title "Milestones"
-- header_title "Milestones", dashboard_milestones_path
+- page_title 'Milestones'
+- header_title 'Milestones', dashboard_milestones_path
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
- = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true
.milestones
%ul.content-list
@@ -15,4 +15,4 @@
- else
- @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index fb5be63b472..13f7a8ddcec 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -1,7 +1,7 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "Activity"
- xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 4f36a4a1c73..eef794dbd51 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,17 +1,17 @@
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+ = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-- if @projects.any? || params[:filter_projects]
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+- if @projects.any? || params[:name]
= render 'dashboard/projects_head'
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any? || params[:filter_projects]
+- if @projects.any? || params[:name]
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 70705923d42..162ae153b1c 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -6,7 +6,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
%h3 You don't have starred projects yet
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index a3993d5ef16..d0c12aa57ae 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -36,9 +36,14 @@
- if todo.pending?
.todo-actions
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
+ = link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do
Done
= icon('spinner spin')
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
Undo
= icon('spinner spin')
+ - else
+ .todo-actions
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
+ Add todo
+ = icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 16a5713948a..d31ced004a0 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -19,9 +19,12 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
= icon('spinner spin')
+ = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
+ Undo mark all as done
+ = icon('spinner spin')
.todos-filters
.row-content-block.second-block
@@ -46,19 +49,19 @@
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
data: { data: todo_actions_options, default_label: 'Action' } })
- .pull-right
- .dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ .filter-item.sort-filter
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-sort
%li
- = link_to todos_filter_path(sort: sort_value_priority) do
- = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_label_priority) do
+ = sort_title_label_priority
= link_to todos_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to todos_filter_path(sort: sort_value_oldest_created) do
@@ -67,12 +70,16 @@
.js-todos-all
- if @todos.any?
- .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- .panel.panel-default.panel-small.panel-without-border
- %ul.content-list.todos-list
- = render @todos
- = paginate @todos, theme: "gitlab"
-
+ .js-todos-list-container
+ .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.todos-list
+ = render @todos
+ = paginate @todos, theme: "gitlab"
+ .js-nothing-here-container.todos-all-done.hidden
+ = render "shared/empty_states/icons/todos_all_done.svg"
+ %h4.text-center
+ You're all done!
- elsif current_user.todos.any?
.todos-all-done
= render "shared/empty_states/icons/todos_all_done.svg"
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 951f03083bf..a039756c7e2 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,6 +1,6 @@
- if inject_u2f_api?
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('u2f.js')
+ = page_specific_javascript_bundle_tag('u2f')
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 5a44ec45b7b..a2f6a7ab1cb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -4,11 +4,11 @@
.devise-errors
= devise_error_messages!
.form-group
- = f.label :name
+ = f.label :name, 'Full name'
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 2deadbeeceb..ee452add394 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -2,5 +2,5 @@
%tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 }
%td.notes_content
- .content
+ .content{ class: ('hide' unless expanded) }
= render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml
new file mode 100644
index 00000000000..ca9e0e8728a
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml
@@ -0,0 +1,6 @@
+- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
+ .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
+ .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue",
+ "aria-label" => "Resolve all discussions in a new issue",
+ "data-container" => "body" }
+ = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
new file mode 100644
index 00000000000..df5546a1e32
--- /dev/null
+++ b/app/views/discussions/_new_issue_for_discussion.html.haml
@@ -0,0 +1,8 @@
+- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
+ %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
+ "inline-template" => true }
+ .btn-group{ role: "group", "v-if" => "showButton" }
+ .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue",
+ "aria-label" => "Resolve this discussion in a new issue",
+ "data-container" => "body" }
+ = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index dfdbdf1f969..2789391819c 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -11,6 +11,8 @@
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
- if discussion.for_merge_request?
- = render "discussions/jump_to_next", discussion: discussion
+ .btn-group.discussion-actions
+ = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
+ = render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index a196561f381..82aa51f9778 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid
@@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
deleted file mode 100644
index 49bd9acd2db..00000000000
--- a/app/views/emojis/index.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-.emoji-menu
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji"
- .emoji-menu-content
- - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
- %h5.emoji-menu-title
- = Gitlab::AwardEmoji::CATEGORIES[category]
- %ul.clearfix.emoji-menu-list
- - emojis.each do |emoji|
- %li.pull-left.text-center.emoji-menu-list-item
- %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
- = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 7890e717aa7..158061579f6 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -4,12 +4,12 @@ xml.entry do
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80)
- xml.updated event.created_at.xmlschema
+ xml.updated event.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do
xml.name event.author_name
- xml.email event.author_email
+ xml.email event.author_public_email
end
xml.summary(type: "xhtml") do |summary|
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index f08c96df309..64b5a733b77 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -15,6 +15,6 @@
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
- else
- = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
+ = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do
%i.fa.fa-paperclip
= note.attachment_identifier
diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml
new file mode 100644
index 00000000000..794c6d1d170
--- /dev/null
+++ b/app/views/explore/groups/_groups.html.haml
@@ -0,0 +1,6 @@
+.js-groups-list-holder
+ %ul.content-list
+ - @groups.each do |group|
+ = render 'shared/groups/group', group: group
+
+ = paginate @groups, theme: 'gitlab'
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
new file mode 100644
index 00000000000..c8d95b52156
--- /dev/null
+++ b/app/views/explore/groups/_nav.html.haml
@@ -0,0 +1,8 @@
+.top-area
+ %ul.nav-links
+ = nav_link(page: explore_groups_path) do
+ = link_to explore_groups_path do
+ Explore Groups
+ .nav-controls
+ = render 'shared/groups/search_form'
+ = render 'shared/groups/dropdown'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 73cf6e87eb4..bb2cd0d44c8 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -5,41 +5,9 @@
= render 'dashboard/groups_head'
- else
= render 'explore/head'
+ = render 'nav'
-.row-content-block.clearfix
- .pull-left
- = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f|
- = hidden_field_tag :sort, @sort
- .form-group
- = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false
- .form-group
- = button_tag 'Search', class: "btn btn-default"
-
- .pull-right
- .dropdown.inline
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to explore_groups_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to explore_groups_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to explore_groups_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to explore_groups_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
-
-%ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
- - unless @groups.present?
- .nothing-here-block No public groups
-
-
-= paginate @groups, theme: "gitlab"
+- if @groups.present?
+ = render 'groups'
+- else
+ .nothing-here-block No public groups
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 614b5431779..e0a2a1e9c96 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,10 +1,17 @@
-%ul.nav-links
- = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
- = link_to trending_explore_projects_path do
- Trending
- = nav_link(page: starred_explore_projects_path) do
- = link_to starred_explore_projects_path do
- Most stars
- = nav_link(page: explore_projects_path) do
- = link_to explore_projects_path do
- All
+.top-area
+ %ul.nav-links
+ = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
+ = link_to trending_explore_projects_path do
+ Trending
+ = nav_link(page: starred_explore_projects_path) do
+ = link_to starred_explore_projects_path do
+ Most stars
+ = nav_link(page: explore_projects_path) do
+ = link_to explore_projects_path do
+ All
+
+ .nav-controls
+ - unless current_user
+ = render 'shared/projects/search_form'
+ = render 'shared/projects/dropdown'
+ = render 'filter'
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 42b50481b9d..ec461755103 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -6,10 +6,5 @@
- else
= render 'explore/head'
-.top-area
- = render 'explore/projects/nav'
-
- .nav-controls
- = render 'filter'
-
+= render 'explore/projects/nav'
= render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 71cc4d87b1f..d7851c79990 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -2,10 +2,9 @@
= render "events/event_last_push", event: @last_push
.nav-block
- - if current_user
- .controls
- = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do
- %i.fa.fa-rss
+ .controls
+ = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
+ %i.fa.fa-rss
= render 'shared/event_filter'
.content_list
diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml
new file mode 100644
index 00000000000..20de1b4c973
--- /dev/null
+++ b/app/views/groups/_create_chat_team.html.haml
@@ -0,0 +1,16 @@
+.form-group
+ = f.label :create_chat_team, class: 'control-label' do
+ %span.mattermost-icon
+ = custom_icon('icon_mattermost')
+ Mattermost
+ .col-sm-10
+ .checkbox.js-toggle-container
+ = f.label :create_chat_team do
+ .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false)
+ Create a Mattermost team for this group
+ %br
+ %small.light.js-toggle-content
+ Mattermost URL:
+ = Settings.mattermost.host
+ %span> /
+ %span{ "data-bind-out" => "create_chat_team" }
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
new file mode 100644
index 00000000000..873504099d4
--- /dev/null
+++ b/app/views/groups/_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
+ = link_to group_path(@group), title: 'Group Home' do
+ %span
+ Home
+
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ %span
+ Activity
diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml
new file mode 100644
index 00000000000..d554bc23743
--- /dev/null
+++ b/app/views/groups/_head_issues.html.haml
@@ -0,0 +1,19 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
+ = link_to issues_group_path(@group), title: 'List' do
+ %span
+ List
+
+ = nav_link(path: 'labels#index') do
+ = link_to group_labels_path(@group), title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(path: 'milestones#index') do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
new file mode 100644
index 00000000000..2454e7355a7
--- /dev/null
+++ b/app/views/groups/_settings_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: 'groups#edit') do
+ = link_to edit_group_path(@group), title: 'General' do
+ %span
+ General
+
+ = nav_link(path: 'groups#projects') do
+ = link_to projects_group_path(@group), title: 'Projects' do
+ %span
+ Projects
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
index aaad265b3ee..3969e56f937 100644
--- a/app/views/groups/activity.html.haml
+++ b/app/views/groups/activity.html.haml
@@ -1,8 +1,8 @@
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+ = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
-- page_title "Activity"
+- page_title "Activity"
+= render 'groups/head'
%section.activities
= render 'activities'
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2706e8692d1..80a77dab97f 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,4 @@
+= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 0cc6466d34e..469768d83f2 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 83edb719692..f4c17dc2d16 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,18 +1,17 @@
- page_title "Issues"
+= render "head_issues"
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
+ = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- if group_issues(@group).exists?
.top-area
= render 'shared/issuable/nav', type: :issues
- - if current_user
- .nav-controls
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
- = icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+ .nav-controls
+ = link_to params.merge(rss_url_options), class: 'btn' do
+ = icon('rss')
+ %span.icon-label
+ Subscribe
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 45325d6bc4b..2bc00fb16c8 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,4 +1,5 @@
- page_title 'Labels'
+= render "groups/head_issues"
.top-area.adjust
.nav-text
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index cd5388fe402..6893168f039 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,7 +1,8 @@
- page_title "Milestones"
+= render "groups/head_issues"
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
- if can?(current_user, :admin_milestones, @group)
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 38d63fd9acc..000c7af2326 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -16,6 +16,8 @@
= render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+
.form-group
.col-sm-offset-2.col-sm-10
= render 'shared/group_tips'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 2e7e5e5c309..83bdd654f27 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,4 +1,4 @@
-- page_title "Projects"
+= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index b68bf444d27..914091dfd15 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -1,7 +1,7 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@group.name} activity"
- xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b040f404ac4..18997baa998 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,9 +1,9 @@
- @no_container = true
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+ = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
+= render 'groups/head'
= render 'groups/home_panel'
@@ -11,8 +11,7 @@
.top-area
= render 'groups/show_nav'
.nav-controls
- = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
+ = render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index 8610ae7e0ef..be809083139 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
+= render 'head'
= render 'groups/home_panel'
.groups-header{ class: container_class }
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 705e20112fa..2684f16c373 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -163,7 +163,7 @@
.key g
.key g
%td
- Go to graphs
+ Go to repository charts
%tr
%td.shortcut
.key g
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 31631887317..f93b6b63426 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -17,7 +17,7 @@
%br
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
- Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+ Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
- if current_application_settings.help_page_text.present?
%hr
= markdown_field(current_application_settings, :help_page_text)
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 87f9b503989..1fb2c6271ad 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -410,7 +410,7 @@
:javascript
$('#js-project-dropdown').glDropdown({
data: function (term, callback) {
- Api.projects(term, "last_activity_at", function (data) {
+ Api.projects(term, { order_by: 'last_activity_at' }, function (data) {
callback(data);
});
},
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index e18bd47798b..e6058617ac9 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -33,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -50,7 +50,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%fieldset.row
.input-group
@@ -70,7 +70,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label label-danger'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d5b88709a34..7456799ca0e 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -43,7 +43,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo["id"]}" }
%td
- = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+ = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
= import_project_target(repo['namespace']['path'], repo['name'])
%td.import-actions.job-status
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 336becd229e..c5800a1cca0 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -13,7 +13,7 @@
%li
%p
Go to
- #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+ #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}.
%li
%p
Make sure you're logged into the account that owns the projects you'd like to import.
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5e01af008be..60de6bfe816 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -36,7 +36,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -53,7 +53,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
@@ -63,7 +63,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag "Incompatible Project", nil, class: "label label-danger"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 96831874144..23a88448055 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -2,12 +2,12 @@ xml.entry do
xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue)
xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue)
xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.xmlschema
+ xml.updated issue.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do
xml.name issue.author_name
- xml.email issue.author_email
+ xml.email issue.author_public_email
end
xml.summary issue.title
@@ -26,7 +26,7 @@ xml.entry do
if issue.assignee
xml.assignee do
xml.name issue.assignee.name
- xml.email issue.assignee.email
+ xml.email issue.assignee_public_email
end
end
end
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 65887aacbaf..04e2d4b63e6 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -2,5 +2,5 @@
%p
= icon('circle', class: 'cgreen')
Integration is active for
- = link_to koding_project_url, target: '_blank' do
+ = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
#{current_application_settings.koding_url}
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 302c1794628..f6d8bb08a64 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,7 +28,9 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
- = javascript_include_tag(*webpack_asset_paths("application"))
+ = javascript_include_tag(*webpack_asset_paths("runtime"))
+ = javascript_include_tag(*webpack_asset_paths("common"))
+ = javascript_include_tag(*webpack_asset_paths("main"))
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 3daa1e90a8c..769f6fb0151 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -4,7 +4,6 @@
- if project
:javascript
gl.GfmAutoComplete.dataSources = {
- emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1717ed6b365..a35a918d501 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,7 +1,7 @@
.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
- %div{ class: container_class }
+ .container-fluid
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 19bd9b6d5c9..36543edc040 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en", class: "#{page_class}" }
= render "layouts/head"
- %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
+ %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
= render "layouts/header/default", title: header_title
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index f8986893776..5fde5c2613e 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -19,7 +19,7 @@
%ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
- %li.visible-sm.visible-xs
+ %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- if current_user
@@ -36,6 +36,10 @@
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_count_format(todos_pending_count)
+ - if current_user.can_create_project?
+ %li
+ = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
@@ -51,8 +55,6 @@
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
= link_to "Settings", profile_path, aria: { label: "Settings" }
- %li
- = link_to "Help", help_path, aria: { label: "Help" }
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
@@ -65,7 +67,7 @@
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
- %h1.title= title
+ %h1.title{ class: ('initializing' if @has_group_title) }= title
= yield :header_content
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
new file mode 100644
index 00000000000..53268cc22f8
--- /dev/null
+++ b/app/views/layouts/mailer.html.haml
@@ -0,0 +1,72 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+ %tbody
+ %tr.line
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
+ %tr.header
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ = header_logo
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
+ %tbody
+ = yield
+
+ %tr.footer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
+ %div
+ %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
+ &middot;
+ %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml
new file mode 100644
index 00000000000..6a9c6ced9cc
--- /dev/null
+++ b/app/views/layouts/mailer.text.haml
@@ -0,0 +1,5 @@
+= yield
+
+You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
+Manage all notifications: #{profile_notifications_url}
+Help: #{help_url}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 19a947af4ca..d068c895fa3 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -33,7 +33,7 @@
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- - if askimet_enabled?
+ - if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5d4178f03d7..15285ee32a3 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -24,16 +24,16 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
%span
Issues
- (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))})
+ .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
%span
Merge Requests
- (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))})
+ .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
Snippets
%li.divider
%li
- = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab'
+ = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f3539fd372d..8605380848d 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,27 +1,14 @@
-= render 'layouts/nav/group_settings'
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
- = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
+ = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
%span
Group
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- %span
- Activity
- = nav_link(controller: [:group, :labels]) do
- = link_to group_labels_path(@group), title: 'Labels' do
- %span
- Labels
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'groups#issues') do
+ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
@@ -33,7 +20,12 @@
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(controller: [:group_members]) do
+ = nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
+ - if current_user && can?(current_user, :admin_group, @group)
+ = nav_link(path: %w[groups#projects groups#edit]) do
+ = link_to edit_group_path(@group), title: 'Settings' do
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
deleted file mode 100644
index 30feb6813b4..00000000000
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- if current_user
- - can_admin_group = can?(current_user, :admin_group, @group)
- - can_edit = can?(current_user, :admin_group, @group)
-
- - if can_admin_group || can_edit
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can_admin_group
- = nav_link(path: 'groups#projects') do
- = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if can_edit && can_admin_group
- %li.divider
- %li
- = link_to 'Edit Group', edit_group_path(@group)
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 7883823b21e..299dace3406 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,60 +1,27 @@
-- if current_user
- .controls
- .dropdown.project-settings-dropdown
- %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - can_edit = can?(current_user, :admin_project, @project)
-
- = render 'layouts/nav/project_settings', can_edit: can_edit
-
- - if can_edit
- %li.divider
- %li
- = link_to edit_project_path(@project) do
- Edit Project
-
+- can_edit = can?(current_user, :admin_project, @project)
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
- = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
+ = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
%span
Project
- = nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- %span
- Activity
-
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do
= link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
Repository
- - if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
-
- if project_nav_tab? :container_registry
= nav_link(controller: %w(container_registry)) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
%span
Registry
- - if project_nav_tab? :graphs
- = nav_link(controller: %w(graphs)) do
- = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
- %span
- Graphs
-
- if project_nav_tab? :issues
= nav_link(controller: [:issues, :labels, :milestones, :boards]) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
@@ -70,6 +37,12 @@
Merge Requests
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
+
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
@@ -82,18 +55,41 @@
%span
Snippets
- -# Global shortcut to network page for compatibility
+ - if project_nav_tab? :settings
+ = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
+ = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
+ %span
+ Settings
+ - else
+ = nav_link(path: %w[members#show]) do
+ = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do
+ %span
+ Settings
+
+ -# Shortcut to Project > Activity
+ %li.hidden
+ = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
+ %span
+ Activity
+
+ -# Shortcut to Repository > Graph (formerly, Network)
- if project_nav_tab? :network
%li.hidden
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
- Network
+ Graph
+
+ -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
+ - unless @project.empty_repo?
+ %li.hidden
+ = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
+ Charts
- -# Shortcut to create a new issue
+ -# Shortcut to Issues > New Issue
%li.hidden
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do
Create a new issue
- -# Shortcut to builds page
+ -# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
%li.hidden
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
deleted file mode 100644
index 665725f6862..00000000000
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- if project_nav_tab? :team
- = nav_link(controller: [:members, :teams]) do
- = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
- %span
- Members
-- if can_edit
- = nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
- %span
- Deploy Keys
- = nav_link(controller: :integrations) do
- = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
- %span
- Integrations
- = nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
- %span
- Protected Branches
-
- - if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :ci_cd) do
- = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
- %span
- CI/CD Pipelines
- = nav_link(controller: :pages) do
- = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
- %span
- Pages
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc69..00000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job failed)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b0..00000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9d..00000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job successful)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64f..00000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index d9ebbaa2704..85a1aea3a61 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -1,179 +1,109 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{ lang: "en" }
- %head
- %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
- %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
- %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
- %title= message.subject
- :css
- /* CLIENT-SPECIFIC STYLES */
- body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
- table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
- img { -ms-interpolation-mode: bicubic; }
-
- /* iOS BLUE LINKS */
- a[x-apple-data-detectors] {
- color: inherit !important;
- text-decoration: none !important;
- font-size: inherit !important;
- font-family: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- }
-
- /* ANDROID MARGIN HACK */
- body { margin:0 !important; }
- div[style*="margin: 16px 0"] { margin:0 !important; }
-
- @media only screen and (max-width: 639px) {
- body, #body {
- min-width: 320px !important;
- }
- table.wrapper {
- width: 100% !important;
- min-width: 320px !important;
- }
- table.wrapper > tbody > tr > td {
- border-left: 0 !important;
- border-right: 0 !important;
- border-radius: 0 !important;
- padding-left: 10px !important;
- padding-right: 10px !important;
- }
- }
- %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+%tr.alert
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
- %tr.line
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
- %tr.header
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+ Your pipeline has failed.
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = namespace_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.ref
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
- %tbody
- %tr.alert
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
- %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- Your pipeline has failed.
- %tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
- %tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
- = namespace_name
- \/
- %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
- = @project.name
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
- = @pipeline.ref
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = @pipeline.short_sha
- - if @merge_request
- in
- %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
- = @merge_request.to_reference
- .commit{ style: "color:#5c5c5c;font-weight:300;" }
- = @pipeline.git_commit_message.truncate(50)
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- - commit = @pipeline.commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- - if commit.author
- %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
- = commit.author.name
- - else
- %span
- = commit.author_name
- %tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
- - failed = @pipeline.statuses.latest.failed
- %tr.pre-section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
- had
- = failed.size
- failed
- #{'build'.pluralize(failed.size)}.
- %tr.warning
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
- Logs may contain sensitive data. Please consider before forwarding this email.
- %tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
- %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
- %tbody
- - failed.each do |build|
- %tr.build-state
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
- %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
- = build.stage
- %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
- = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
- %tr.build-log
- - if build.has_trace?
- %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
- %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
- = build.trace_html(last_lines: 10).html_safe
- - else
- %td{ colspan: "2" }
- %tr.footer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
- %div
- %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
- &middot;
- %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
- %div
- You're receiving this email because of your account on
- = succeed "." do
- %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
+ = @merge_request.to_reference
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.author
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+- failed = @pipeline.statuses.latest.failed
+%tr.pre-section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ had
+ = failed.size
+ failed
+ #{'build'.pluralize(failed.size)}.
+%tr.warning
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
+ Logs may contain sensitive data. Please consider before forwarding this email.
+%tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
+ %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
+ = build.stage
+ %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+ = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
+ %tr.build-log
+ - if build.has_trace?
+ %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
+ %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
+ = build.trace_html(last_lines: 10).html_safe
+ - else
+ %td{ colspan: "2" }
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index ab91c7ef350..520a2fc7d68 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
<% end -%>
<% end -%>
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 8add2e18206..19d4add06f5 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,154 +1,84 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{ lang: "en" }
- %head
- %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
- %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
- %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
- %title= message.subject
- :css
- /* CLIENT-SPECIFIC STYLES */
- body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
- table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
- img { -ms-interpolation-mode: bicubic; }
-
- /* iOS BLUE LINKS */
- a[x-apple-data-detectors] {
- color: inherit !important;
- text-decoration: none !important;
- font-size: inherit !important;
- font-family: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- }
-
- /* ANDROID MARGIN HACK */
- body { margin:0 !important; }
- div[style*="margin: 16px 0"] { margin:0 !important; }
-
- @media only screen and (max-width: 639px) {
- body, #body {
- min-width: 320px !important;
- }
- table.wrapper {
- width: 100% !important;
- min-width: 320px !important;
- }
- table.wrapper > tbody > tr > td {
- border-left: 0 !important;
- border-right: 0 !important;
- border-radius: 0 !important;
- padding-left: 10px !important;
- padding-right: 10px !important;
- }
- }
- %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+%tr.success
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
- %tr.line
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
- %tr.header
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+ Your pipeline has passed.
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = namespace_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.ref
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
+ = @merge_request.to_reference
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
- %tbody
- %tr.success
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
- %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- Your pipeline has passed.
- %tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
- %tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
- = namespace_name
- \/
- %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
- = @project.name
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
- = @pipeline.ref
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = @pipeline.short_sha
- - if @merge_request
- in
- %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
- = @merge_request.to_reference
- .commit{ style: "color:#5c5c5c;font-weight:300;" }
- = @pipeline.git_commit_message.truncate(50)
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- - commit = @pipeline.commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- - if commit.author
- %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
- = commit.author.name
- - else
- %span
- = commit.author_name
- %tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
- %tr.success-message
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
- - build_count = @pipeline.statuses.latest.size
- - stage_count = @pipeline.stages_count
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
- successfully completed
- #{build_count} #{'build'.pluralize(build_count)}
- in
- #{stage_count} #{'stage'.pluralize(stage_count)}.
- %tr.footer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
- %div
- %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
- &middot;
- %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
- %div
- You're receiving this email because of your account on
- = succeed "." do
- %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
+ - commit = @pipeline.commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.author
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.success-message
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
+ - build_count = @pipeline.statuses.latest.size
+ - stage_count = @pipeline.stages_count
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ successfully completed
+ #{build_count} #{'build'.pluralize(build_count)}
+ in
+ #{stage_count} #{'stage'.pluralize(stage_count)}.
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 40e5e306426..0970a3a4e09 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %>
<% build_count = @pipeline.statuses.latest.size -%>
<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
index 1df04ea614e..83ae9129807 100644
--- a/app/views/profiles/_head.html.haml
+++ b/app/views/profiles/_head.html.haml
@@ -1,3 +1,2 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/cropper.js')
= page_specific_javascript_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index a4f4079d556..8a994f6d600 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -93,7 +93,7 @@
%p
Changing your username will change path to all personal projects!
.col-lg-9
- = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f|
+ = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
= f.label :username, "Path", class: "label-light"
.input-group
@@ -115,7 +115,7 @@
%h4.prepend-top-0.danger-title
Remove account
.col-lg-9
- - if @user.can_be_removed?
+ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
Deleting an account has the following effects:
%ul
@@ -131,4 +131,7 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete your account.
+ - else
+ %p
+ You don't have access to delete this user.
.append-bottom-default
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 51c4e8e5a73..5c5e5940365 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,11 +34,6 @@
.clearfix
- = form_for @user, url: profile_notifications_path, method: :put do |f|
- %label{ for: 'user_notified_of_own_activity' }
- = f.check_box :notified_of_own_activity
- %span Receive notifications about your own activity
-
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
deleted file mode 100644
index 3f6efa33953..00000000000
--- a/app/views/profiles/personal_access_tokens/_form.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- personal_access_token = local_assigns.fetch(:personal_access_token)
-- scopes = local_assigns.fetch(:scopes)
-
-= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
-
- = form_errors(personal_access_token)
-
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
-
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control"
-
- .form-group
- = f.label :scopes, class: 'label-light'
- = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
-
- .prepend-top-default
- = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index b10f5fc08e2..0645ecad496 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -24,82 +24,11 @@
%hr
- %h5.prepend-top-0
- Add a Personal Access Token
- %p.profile-settings-content
- Pick a name for the application, and we'll give you a unique token.
-
- = render "form", personal_access_token: @personal_access_token, scopes: @scopes
-
- %hr
-
- %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
-
- - if @active_personal_access_tokens.present?
- .table-responsive
- %table.table.active-personal-access-tokens
- %thead
- %tr
- %th Name
- %th Created
- %th Expires
- %th Scopes
- %th
- %tbody
- - @active_personal_access_tokens.each do |token|
- %tr
- %td= token.name
- %td= token.created_at.to_date.to_s(:medium)
- %td
- - if token.expires_at.present?
- = token.expires_at.to_date.to_s(:medium)
- - else
- %span.personal-access-tokens-never-expires-label Never
- %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
-
- - else
- .settings-message.text-center
- You don't have any active tokens yet.
-
- %hr
-
- %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
-
- - if @inactive_personal_access_tokens.present?
- .table-responsive
- %table.table.inactive-personal-access-tokens
- %thead
- %tr
- %th Name
- %th Created
- %tbody
- - @inactive_personal_access_tokens.each do |token|
- %tr
- %td= token.name
- %td= token.created_at.to_date.to_s(:medium)
-
- - else
- .settings-message.text-center
- There are no inactive tokens.
+ = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
+ = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
:javascript
- var $dateField = $('#personal_access_token_expires_at');
- var date = $dateField.val();
-
- new Pikaday({
- field: $dateField.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- minDate: new Date(),
- onSelect: function(dateText) {
- $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
-
$("#created-personal-access-token").click(function() {
this.select();
});
-
- $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d551754a2e5..c74b3249a13 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,7 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 558a1d56151..7ade5f00d47 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -4,7 +4,7 @@
- if inject_u2f_api?
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('u2f.js')
+ = page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default
.col-lg-3
@@ -96,4 +96,3 @@
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
-
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
deleted file mode 100644
index 5307e0b48cb..00000000000
--- a/app/views/profiles/update_username.js.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- if @user.valid?
- :plain
- new Flash("Username successfully changed", "notice")
-- else
- - error = @user.errors.full_messages.first
- :plain
- new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 0ea733cb978..aa0cb3e1a50 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -2,10 +2,9 @@
%div{ class: container_class }
.nav-block.activity-filter-block
- - if current_user
- .controls
- = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
- = icon('rss')
+ .controls
+ = link_to namespace_project_path(@project.namespace, @project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
+ = icon('rss')
= render 'shared/event_filter'
diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml
new file mode 100644
index 00000000000..db08b77c8e0
--- /dev/null
+++ b/app/views/projects/_head.html.haml
@@ -0,0 +1,20 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: 'projects#show') do
+ = link_to project_path(@project), title: 'Project home', class: 'shortcuts-project' do
+ %span
+ Home
+
+ = nav_link(path: 'projects#activity') do
+ = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
+ %span
+ Activity
+
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(path: 'cycle_analytics#show') do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics', class: 'shortcuts-project-cycle-analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 27d25a6b682..61420fd0fb6 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -2,8 +2,8 @@
.form-group
.checkbox.builds-feature
- = form.label :only_allow_merge_if_build_succeeds do
- = form.check_box :only_allow_merge_if_build_succeeds
+ = form.label :only_allow_merge_if_pipeline_succeeds do
+ = form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds
%br
%span.descr
@@ -13,3 +13,7 @@
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved
+ .checkbox
+ = form.label :printing_merge_request_link_enabled do
+ = form.check_box :printing_merge_request_link_enabled
+ %strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 3c0f01cbf6f..27c8e3c7fca 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,5 @@
- page_title "Activity"
+= render "projects/head"
= render 'projects/last_push'
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8a40281e28c..4ad77b6266d 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,13 +7,8 @@
#blob-content-holder.tree-holder
.file-holder
- .js-file-title.file-title
- = blob_icon @blob.mode, @blob.name
- %strong
- = @path
- %small= number_to_human_size @blob.size
- .file-actions
- = render "projects/blob/actions"
+ = render "projects/blob/header", blob: @blob
+
.table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
deleted file mode 100644
index 7b9cfbbd067..00000000000
--- a/app/views/projects/blob/_actions.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-.btn-group
- = view_on_environment_button(@commit.sha, @path, @environment) if @environment
-
-.btn-group.tree-btn-group
- = 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_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'
- - else
- = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
- class: 'btn btn-sm' unless @blob.empty?
- = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
- class: 'btn btn-sm'
- = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
-
-- if current_user
- .btn-group{ role: "group" }
- - if blob_text_viewable?(@blob)
- = edit_blob_link
- = replace_blob_link
- = delete_blob_link
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 19fa4c78501..2b2ee6ed987 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,18 +18,11 @@
- else
= link_to title, '#'
-%ul.blob-commit-info.table-list.hidden-xs
+%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
#blob-content-holder.blob-content-holder
%article.file-holder
- .js-file-title.file-title
- = blob_icon blob.mode, blob.name
- %strong
- = blob.name
- %small
- = number_to_human_size(blob_size(blob))
- .file-actions.hidden-xs
- = render "actions"
- = render blob, blob: blob
+ = render "projects/blob/header", blob: blob
+ = render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
new file mode 100644
index 00000000000..deeeae3d64a
--- /dev/null
+++ b/app/views/projects/blob/_header.html.haml
@@ -0,0 +1,39 @@
+.js-file-title.file-title-flex-parent
+ .file-header-content
+ = blob_icon blob.mode, blob.name
+
+ %strong.file-title-name
+ = blob.name
+
+ = copy_file_path_button(blob.path)
+
+ %small
+ = number_to_human_size(blob_size(blob))
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = copy_blob_content_button(blob) if blob_text_viewable?(blob)
+ = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
+ .btn-group{ role: "group" }<
+ -# only show normal/blame view links for text files
+ - 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'
+ - else
+ = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
+ class: 'btn btn-sm js-blob-blame-link' unless blob.empty?
+
+ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
+ class: 'btn btn-sm'
+
+ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
+
+ - if current_user
+ .btn-group{ role: "group" }<
+ = edit_blob_link if blob_text_viewable?(blob)
+ = replace_blob_link
+ = delete_blob_link
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index f864702d862..ea3cecb86a9 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -9,7 +9,7 @@
- else
.nothing-here-block
The SVG could not be displayed as it is too large, you can
- #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+ #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
instead.
- else
%img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 58524418a67..7b16d266982 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -3,17 +3,17 @@
.nothing-here-block
File too large, you can
= succeed '.' do
- = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank'
+ = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
- else
- blob.load_all_data!(@repository)
- - if markup?(blob.name)
- .file-content.wiki
- = render_markup(blob.name, blob.data)
+ - if blob.empty?
+ .file-content.code
+ .nothing-here-block Empty file
- else
- - if blob.empty?
- .file-content.code
- .nothing-here-block Empty file
+ - if markup?(blob.name)
+ .file-content.wiki
+ = render_markup(blob.name, blob.data)
- else
= render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index d1f7f65bf53..d1d448f0d4c 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -9,20 +9,20 @@
- line_old = line_new - @form.offset
- line_content = capture do
%td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line}
- %tr.line_holder{ id: line_old, class: line_class }
+ %tr.line_holder.diff-expanded{ id: line_old, class: line_class }
- case diff_view
- when :inline
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- %a{ href: "##{line_old}", data: { linenumber: line_old } }
+ %a{ href: "#", data: { linenumber: line_old }, disabled: true }
%td.new_line.diff-line-num{ data: { linenumber: line_new } }
- %a{ href: "##{line_new}", data: { linenumber: line_new } }
+ %a{ href: "#", data: { linenumber: line_new }, disabled: true }
= line_content
- when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_old), "##{line_old}"
+ %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true }
= line_content
%td.new_line.diff-line-num{ data: { linenumber: line_new } }
- = link_to raw(line_new), "##{line_new}"
+ %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true }
= line_content
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 8853801016b..3bcddcb37f1 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.file-editor
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index f5ca9607823..added3f669b 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,16 +3,19 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('boards')
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
- %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
+ %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
-= render 'shared/issuable/filter', type: :boards
+.hidden-xs.hidden-sm
+ = render 'shared/issuable/search_bar', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
deleted file mode 100644
index 0af40ddf8fe..00000000000
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%board-blank-state{ "inline-template" => true,
- "v-if" => 'list.id == "blank"' }
- .board-blank-state
- %p
- Add the following default lists to your Issue Board with one click:
- %ul.board-blank-state-list
- %li{ "v-for" => "label in predefinedLabels" }
- %span.label-color{ ":style" => "{ backgroundColor: label.color } " }
- {{ label.title }}
- %p
- Starting out with the default set of lists will get you right on the way to making the most of your board.
- %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
- Add default lists
- %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
- Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 72bce4049de..0bca6a786cb 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -32,4 +32,4 @@
":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index f413a5e94c1..4a4dd84d5d2 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -2,33 +2,13 @@
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
- %board-new-issue{ "inline-template" => true,
- ":list" => "list",
+ %board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "done" && showIssueForm' }
- .card.board-new-issue-form
- %form{ "@submit" => "submit($event)" }
- .flash-container{ "v-if" => "error" }
- .flash-alert
- An error occured. Please try again.
- %label.label-light{ ":for" => 'list.id + "-title"' }
- Title
- %input.form-control{ type: "text",
- "v-model" => "title",
- "ref" => "input",
- ":id" => 'list.id + "-title"' }
- .clearfix.prepend-top-10
- %button.btn.btn-success.pull-left{ type: "submit",
- ":disabled" => 'title === ""',
- "ref" => "submit-button" }
- Submit issue
- %button.btn.btn-default.pull-right{ type: "button",
- "@click" => "cancel" }
- Cancel
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
- %board-card{ "v-for" => "(issue, index) in orderedIssues",
+ %board-card{ "v-for" => "(issue, index) in issues",
"ref" => "issue",
":index" => "index",
":list" => "list",
@@ -37,7 +17,8 @@
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
- %li.board-list-count.text-center{ "v-if" => "showCount" }
+ %li.board-list-count.text-center{ "v-if" => "showCount",
+ "data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
deleted file mode 100644
index 891c2c46251..00000000000
--- a/app/views/projects/boards/components/_card.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
- ":index" => "index",
- ":data-issue-id" => "issue.id",
- "@mousedown" => "mouseDown",
- "@mousemove" => "mouseMove",
- "@mouseup" => "showIssue($event)" }
- %issue-card-inner{ ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath" }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19ffe73a08d..9eb610ba9c0 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -27,11 +27,11 @@
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
Compare
- = render 'projects/buttons/download', project: @project, ref: branch.name
+ = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
- if can?(current_user, :push_code, @project)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
- class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
+ class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
method: :delete,
data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
remote: true,
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e63bdb38bd8..d3c3e40d518 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -12,12 +12,16 @@
.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 js-branch-name'
+ = text_field_tag :branch_name, params[:branch_name], required: true, 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
- = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ = hidden_field_tag :ref, params[:ref] || @project.default_branch
+ = dropdown_tag(params[:ref] || @project.default_branch,
+ options: { toggle_class: 'js-branch-select wide',
+ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+ data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
.help-block Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 27e81c2bec3..7eb17e887e7 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,4 +1,4 @@
-.content-block.build-header
+.content-block.build-header.top-area
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
Job
@@ -16,7 +16,10 @@
- if @build.user
= render "user"
= time_ago_with_tooltip(@build.created_at)
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
+ .nav-controls
+ - if can?(current_user, :create_issue, @project) && @build.failed?
+ = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 228dad528ab..307010edb58 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs"
-- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index b560ed21f1d..d90d4a27cd6 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,3 +1,5 @@
+- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown' }
@@ -24,7 +26,6 @@
%i.fa.fa-download
%span Download tar
- - pipeline = project.pipelines.latest_successful_for(ref)
- if pipeline
- artifacts = pipeline.builds.latest.with_artifacts
- if artifacts.any?
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index 5d9a776da89..a5a9e4d0621 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,3 +1,3 @@
- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
- = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+ = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 5ea85f9fd4c..09286a1b3c6 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -46,7 +46,7 @@
%span.label.label-info triggered
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
- - if build.manual?
+ - if build.action?
%span.label.label-info manual
- if pipeline_link
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
deleted file mode 100644
index 3475fa5f960..00000000000
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ /dev/null
@@ -1,92 +0,0 @@
-- status = pipeline.status
-- show_commit = local_assigns.fetch(:show_commit, true)
-- show_branch = local_assigns.fetch(:show_branch, true)
-
-%tr.commit
- %td.commit-link
- = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
-
- %td
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- %span.pipeline-id ##{pipeline.id}
- %span by
- - if pipeline.user
- = user_avatar(user: pipeline.user, size: 20)
- - else
- %span.api.monospace API
- - if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- - if pipeline.triggered?
- %span.label.label-primary triggered
- - if pipeline.yaml_errors.present?
- %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- - if pipeline.builds.any?(&:stuck?)
- %span.label.label-warning stuck
-
- %td.branch-commit
- - if pipeline.ref && show_branch
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- - if show_commit
- .icon-container.commit-icon
- = custom_icon("icon_commit")
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
-
- %p.commit-title
- - if commit = pipeline.commit
- = author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- - else
- Cant find HEAD commit for this branch
-
- %td
- = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
-
- %td
- - if pipeline.duration
- %p.duration
- = custom_icon("icon_timer")
- = duration_in_numbers(pipeline.duration)
- - if pipeline.finished_at
- %p.finished-at
- = icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
-
- %td.pipeline-actions.hidden-xs
- .controls.pull-right
- - artifacts = pipeline.builds.latest.with_artifacts_not_expired
- - actions = pipeline.manual_actions
- - if artifacts.present? || actions.any?
- .btn-group.inline
- - if actions.any?
- .btn-group
- %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
- = custom_icon('icon_play')
- = icon('caret-down', 'aria-hidden' => 'true')
- %ul.dropdown-menu.dropdown-menu-align-right
- - actions.each do |build|
- %li
- = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
- = custom_icon('icon_play')
- %span= build.name
- - if artifacts.present?
- .btn-group
- %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
- = icon("download")
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - artifacts.each do |build|
- %li
- = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
- = icon("download")
- %span Download '#{build.name}' artifacts
-
- - if can?(current_user, :update_pipeline, pipeline.project)
- .cancel-retry-btns.inline
- - if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do
- = icon("repeat")
- - if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
- = icon("remove")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 421b3db342d..b5f67cae341 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -1,10 +1,10 @@
- case type.to_s
- when 'revert'
- label = 'Revert'
- - target_label = 'Revert in branch'
+ - branch_label = 'Revert in branch'
- when 'cherry-pick'
- label = 'Cherry-pick'
- - target_label = 'Pick into branch'
+ - branch_label = 'Pick into branch'
.modal{ id: "modal-#{type}-commit" }
.modal-dialog
@@ -15,10 +15,10 @@
.modal-body
= form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
- = label_tag 'target_branch', target_label, class: 'control-label'
+ = label_tag 'start_branch', branch_label, class: 'control-label'
.col-sm-10
- = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
- = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
+ = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
@@ -37,4 +37,4 @@
= commit_in_fork_help
:javascript
- new NewCommitForm($('.js-#{type}-form'))
+ new NewCommitForm($('.js-#{type}-form'), 'start_branch')
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 4d0b7a5ca85..a0a292d0508 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -34,8 +34,9 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
- %li.clearfix
- = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
+ - if can_collaborate_with_project?
+ %li.clearfix
+ = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
%li.dropdown-header
Download
@@ -62,15 +63,15 @@
- if @commit.status
.well-segment.pipeline-info
- %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+ .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do
= ci_icon_for_status(@commit.status)
Pipeline
- = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
- for
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- %span.ci-status-label
- = ci_label_for_status(@commit.status)
+ = link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace"
+ = ci_label_for_status(@commit.status)
+ - if @commit.latest_pipeline.stages.any?
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph'
in
= time_interval_in_words @commit.pipelines.total_duration
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 33917513f37..da5a676274f 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -2,27 +2,7 @@
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
} }
-.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"),
- "icon_status_canceled" => custom_icon("icon_status_canceled"),
- "icon_status_running" => custom_icon("icon_status_running"),
- "icon_status_skipped" => custom_icon("icon_status_skipped"),
- "icon_status_created" => custom_icon("icon_status_created"),
- "icon_status_pending" => custom_icon("icon_status_pending"),
- "icon_status_success" => custom_icon("icon_status_success"),
- "icon_status_failed" => custom_icon("icon_status_failed"),
- "icon_status_warning" => custom_icon("icon_status_warning"),
- "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
- "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
- "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
- "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
- "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
- "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
- "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
- "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
- "icon_play" => custom_icon("icon_play"),
- "icon_timer" => custom_icon("icon_timer"),
- "icon_status_manual" => custom_icon("icon_status_manual"),
-} }
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 002e3d345dc..6ab9a80e083 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,33 +9,34 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
+ %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .table-list-cell.avatar-cell.hidden-xs
+ .avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
- .table-list-cell.commit-content
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
- %span.commit-row-message.visible-xs-inline
- &middot;
- = commit.short_id
- - if commit.status(ref)
- .visible-xs-inline
- = render_commit_status(commit, ref: ref)
- - if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ .commit-detail
+ .commit-content
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status(ref)
+ .visible-xs-inline
+ = render_commit_status(commit, ref: ref)
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
- .commiter
- = commit_author_link(commit, avatar: false, size: 24)
- committed
- #{time_ago_with_tooltip(commit.committed_date)}
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content
+ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
+ .commiter
+ = commit_author_link(commit, avatar: false, size: 24)
+ committed
+ #{time_ago_with_tooltip(commit.committed_date)}
- .table-list-cell.commit-actions.hidden-xs
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
- = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
- = link_to_browse_code(project, commit)
+ .commit-actions.flex-row.hidden-xs
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
+ = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 64d93e4141c..6f5835cb9be 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.content-list.table-list= render commits, project: @project, ref: @ref
+ %ul.content-list= render commits, project: @project, ref: @ref
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 904cdb5767f..88c7d7bc44b 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
- %ul.content-list.commit-list.table-list.table-wide
+ %ul.content-list.commit-list
= render commits, project: project, ref: ref
- if hidden > 0
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 80763ce67ca..dd6797f10c0 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -11,14 +11,6 @@
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
- Network
-
- = nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
- Compare
-
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
Branches
@@ -26,3 +18,19 @@
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
+
+ = nav_link(path: 'graphs#show') do
+ = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do
+ Contributors
+
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+ Graph
+
+ = nav_link(controller: :compare) do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
+ Compare
+
+ = nav_link(path: 'graphs#charts') do
+ = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do
+ Charts
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 30bb7412073..2f0b6e39800 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -1,7 +1,7 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@project.name}:#{@ref} commits"
- xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
xml.updated @commits.first.committed_date.xmlschema if @commits.any?
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 08cb8a04413..38dbf2ac10b 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -2,8 +2,7 @@
- page_title "Commits", @ref
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= content_for :sub_nav do
= render "head"
@@ -27,10 +26,9 @@
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
= search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
- - if current_user && current_user.private_token
- .control
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
- = icon("rss")
+ .control
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits Feed", class: 'btn' do
+ = icon("rss")
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index c8f0b547f80..9007f2c24ba 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -9,7 +9,7 @@
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
- %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+ %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 5405ff16bea..dd3fa814716 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,9 +1,10 @@
- @no_container = true
- page_title "Cycle Analytics"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('cycle_analytics')
-= render "projects/pipelines/head"
+= render "projects/head"
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index d1e3cb14022..ec8fc4c9ee8 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -18,7 +18,7 @@
%span.key-created-at
created #{time_ago_with_tooltip(deploy_key.created_at)}
.visible-xs-block.visible-sm-block
- - if @available_keys.include?(deploy_key)
+ - if @deploy_keys.key_available?(deploy_key)
= link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do
Enable
- else
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index c91bb9c255a..1421da72418 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,5 +1,5 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
- = form_errors(@key)
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
+ = form_errors(@deploy_keys.new_key)
.form-group
= f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', autofocus: true, required: true
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
new file mode 100644
index 00000000000..4cfbd9add00
--- /dev/null
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -0,0 +1,34 @@
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Deploy Keys
+ %p
+ Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
+ .col-lg-9
+ %h5.prepend-top-0
+ Create a new deploy key for this project
+ = render @deploy_keys.form_partial_path
+ .col-lg-9.col-lg-offset-3
+ %hr
+ .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
+ %h5.prepend-top-0
+ Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size})
+ - if @deploy_keys.any_keys_enabled?
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key
+ - else
+ .settings-message.text-center
+ No deploy keys found. Create one with the form above.
+ %h5.prepend-top-default
+ Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size})
+ - if @deploy_keys.any_available_project_keys_enabled?
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
+ - else
+ .settings-message.text-center
+ No deploy keys from your projects could be found. Create one with the form above or add existing one below.
+ - if @deploy_keys.any_available_public_keys_enabled?
+ %h5.prepend-top-default
+ Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
deleted file mode 100644
index 04fbb37d93f..00000000000
--- a/app/views/projects/deploy_keys/index.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- page_title "Deploy Keys"
-
-.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = page_title
- %p
- Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .col-lg-9
- %h5.prepend-top-0
- Create a new deploy key for this project
- = render "form"
- .col-lg-9.col-lg-offset-3
- %hr
- .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
- %h5.prepend-top-0
- Enabled deploy keys for this project (#{@enabled_keys.size})
- - if @enabled_keys.any?
- %ul.well-list
- = render @enabled_keys
- - else
- .settings-message.text-center
- No deploy keys found. Create one with the form above or add existing one below.
- %h5.prepend-top-default
- Deploy keys from projects you have access to (#{@available_project_keys.size})
- - if @available_project_keys.any?
- %ul.well-list
- = render @available_project_keys
- - else
- .settings-message.text-center
- No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- - if @available_public_keys.any?
- %h5.prepend-top-default
- Public deploy keys available to any project (#{@available_public_keys.size})
- %ul.well-list
- = render @available_public_keys
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index a680b1ca017..506246f2ee6 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,9 +1,9 @@
- if can?(current_user, :create_deployment, deployment)
- actions = deployment.manual_actions
- if actions.present?
- .inline
+ .btn-group
.dropdown
- %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -12,4 +12,3 @@
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
-
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index c468202569f..260c9023daf 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -17,6 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs
- .pull-right
+ .pull-right.btn-group
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 1dbfe830d52..7d6b3701f95 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -2,18 +2,21 @@
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
- %span
+
+ %strong.file-title-name
= submodule_link(blob, diff_commit.id, project.repository)
+
+ = copy_file_path_button(blob.path)
- else
= conditional_link_to url.present?, url do
= blob_icon diff_file.b_mode, diff_file.file_path
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path
&rarr;
- %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
@@ -21,7 +24,7 @@
- if diff_file.deleted_file
deleted
- = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+ = copy_file_path_button(diff_file.new_path)
- if diff_file.mode_changed?
%small
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index cd18ba2ed00..c09c7b87e24 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,22 +1,27 @@
- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
+- discussions = local_assigns.fetch(:discussions, nil)
- type = line.type
- line_code = diff_file.line_code(line)
-%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
+- if discussions && !line.meta?
+ - discussion = discussions[line_code]
+%tr.line_holder{ class: type, id: (line_code unless plain) }
- case type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
+ %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } }
- link_text = type == "new" ? " " : line.old_pos
- if plain
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
+ - if discussion && discussion.resolvable? && !plain
+ %diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
@@ -29,9 +34,6 @@
- else
= diff_line_content(line.text)
-- discussions = local_assigns.fetch(:discussions, nil)
-- if discussions && !line.meta?
- - discussion = discussions[line_code]
- - if discussion
- - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
- = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
+- if discussion
+ - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
+ = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 997bf0fc560..b7346f27ddb 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -4,19 +4,24 @@
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
+ - last_line = right.new_pos if right
+ - unless @diff_notes_disabled
+ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
- case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.line_content.match= left.text
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
- %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
+ - if discussion_left && discussion_left.resolvable?
+ %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
@@ -26,23 +31,23 @@
- case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- - when 'nonewline'
+ - when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num
%td.line_content.match= right.text
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
- %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
+ - if discussion_right && discussion_right.resolvable?
+ %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
- - unless @diff_notes_disabled
- - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
- - if discussion_left || discussion_right
- = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+ - if discussion_left || discussion_right
+ = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b9300efd04f..2802a4eca7b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,3 +1,4 @@
+= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
@@ -120,7 +121,7 @@
.form-group
- if @project.avatar?
.avatar-container.s160
- = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+ = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160')
%p.light
- if @project.avatar_in_git
Project avatar in repository: #{ @project.avatar_in_git }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 58c085cdb9d..85e442e115c 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -5,6 +5,7 @@
= render 'shared/no_ssh'
= render 'shared/no_password'
+= render "projects/head"
= render "home_panel"
.row-content-block.second-block.center
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index 4c8fe1c271b..bf0f1819073 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,3 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+ = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link')
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
new file mode 100644
index 00000000000..acbac1869fd
--- /dev/null
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -0,0 +1,6 @@
+- environment = local_assigns.fetch(:environment)
+
+- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
+
+= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
+ = icon('area-chart')
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index d9cb7bc0331..4b101447bc0 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -3,6 +3,7 @@
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 1f27d41ddd9..80d2b6f5d95 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,6 +3,7 @@
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
@@ -13,7 +14,4 @@
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
"help-page-path" => help_page_path("ci/environments"),
- "css-class" => container_class,
- "commit-icon-svg" => custom_icon("icon_commit"),
- "terminal-icon-svg" => custom_icon("icon_terminal"),
- "play-icon-svg" => custom_icon("icon_play") } }
+ "css-class" => container_class } }
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
new file mode 100644
index 00000000000..b8c1782f050
--- /dev/null
+++ b/app/views/projects/environments/metrics.html.haml
@@ -0,0 +1,24 @@
+- @no_container = true
+- page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('monitoring')
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Environment:
+ = @environment.name
+
+ .col-sm-6
+ .nav-controls
+ = render 'projects/deployments/actions', deployment: @environment.last_deployment
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7036325fff8..f463a429f65 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,6 +8,7 @@
%h3.page-title= @environment.name
.col-md-3
.nav-controls
+ = render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
@@ -15,7 +16,7 @@
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
- .deployments-container
+ .environments-container
- if @deployments.blank?
.blank-state.blank-state-no-icon
%h2.blank-state-title
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
deleted file mode 100644
index 67018aaa2ac..00000000000
--- a/app/views/projects/graphs/_head.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
-
- - content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_chart')
- = page_specific_javascript_bundle_tag('graphs')
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.feature_available?(:builds, current_user)
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
new file mode 100644
index 00000000000..464ac34d961
--- /dev/null
+++ b/app/views/projects/graphs/charts.html.haml
@@ -0,0 +1,127 @@
+- @no_container = true
+- page_title "Charts"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('graphs')
+= render "projects/commits/head"
+
+.repo-charts{ class: container_class }
+ %h4.sub-header
+ Programming languages used in this repository
+
+ .row
+ .col-md-4
+ %ul.bordered-list
+ - @languages.each do |language|
+ %li
+ %span{ style: "color: #{language[:color]}" }
+ = icon('circle')
+ &nbsp;
+ = language[:label]
+ .pull-right
+ = language[:value]
+ \%
+ .col-md-8
+ %canvas#languages-chart{ height: 400 }
+
+.repo-charts{ class: container_class }
+ .sub-header-block.border-top
+
+ .row.tree-ref-header
+ .col-md-6
+ %h4
+ Commit statistics for
+ %strong= @ref
+ #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+
+ .col-md-6
+ .tree-ref-container
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
+ .row
+ .col-md-6
+ %ul.commit-stats
+ %li
+ Total:
+ %strong #{@commits_graph.commits.size} commits
+ %li
+ Average per day:
+ %strong #{@commits_graph.commit_per_day} commits
+ %li
+ Authors:
+ %strong= @commits_graph.authors
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day of month
+ %canvas#month-chart
+ .row
+ .col-md-6
+ .col-md-6
+ %div
+ %p.slead
+ Commits per weekday
+ %canvas#weekday-chart
+ .row
+ .col-md-6
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day hour (UTC)
+ %canvas#hour-chart
+
+:javascript
+ var responsiveChart = function (selector, data) {
+ var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false };
+ // get selector by context
+ var ctx = selector.get(0).getContext("2d");
+ // pointing parent container to make chart.js inherit its width
+ var container = $(selector).parent();
+ var generateChart = function() {
+ selector.attr('width', $(container).width());
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8
+ }
+ return new Chart(ctx).Bar(data, options);
+ };
+ // enabling auto-resizing
+ $(window).resize(generateChart);
+ return generateChart();
+ };
+
+ var chartData = function (keys, values) {
+ var data = {
+ labels : keys,
+ datasets : [{
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data : values
+ }]
+ };
+ return data;
+ };
+
+ var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json});
+ responsiveChart($('#hour-chart'), hourData);
+
+ var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json});
+ responsiveChart($('#weekday-chart'), dayData);
+
+ var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json});
+ responsiveChart($('#month-chart'), monthData);
+
+ var data = #{@languages.to_json};
+ var ctx = $("#languages-chart").get(0).getContext("2d");
+ var options = {
+ scaleOverlay: true,
+ responsive: true,
+ maintainAspectRatio: false
+ }
+ var myPieChart = new Chart(ctx).Pie(data, options);
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
deleted file mode 100644
index 6be4273b6ab..00000000000
--- a/app/views/projects/graphs/ci.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- @no_container = true
-- page_title "Continuous Integration", "Graphs"
-= render 'head'
-
-%div{ class: container_class }
- .sub-header-block
- .oneline
- A collection of graphs for Continuous Integration
-
- #charts.ci-charts
- .row
- .col-md-6
- = render 'projects/graphs/ci/overall'
- .col-md-6
- = render 'projects/graphs/ci/build_times'
-
- %hr
- = render 'projects/graphs/ci/builds'
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
deleted file mode 100644
index c8a82f7bca3..00000000000
--- a/app/views/projects/graphs/commits.html.haml
+++ /dev/null
@@ -1,95 +0,0 @@
-- @no_container = true
-- page_title "Commits", "Graphs"
-= render 'head'
-
-%div{ class: container_class }
- .sub-header-block
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
-
- %p.lead
- Commit statistics for
- %strong= @ref
- #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
-
- .row
- .col-md-6
- %ul
- %li
- %p.lead
- %strong= @commits_graph.commits.size
- commits during
- %strong= @commits_graph.duration
- days
- %li
- %p.lead
- Average
- %strong= @commits_graph.commit_per_day
- commits per day
- %li
- %p.lead
- Contributed by
- %strong= @commits_graph.authors
- authors
- .col-md-6
- %div
- %p.slead
- Commits per day of month
- %canvas#month-chart
- .row
- .col-md-6
- %div
- %p.slead
- Commits per day hour (UTC)
- %canvas#hour-chart
- .col-md-6
- %div
- %p.slead
- Commits per weekday
- %canvas#weekday-chart
-
-:javascript
- var responsiveChart = function (selector, data) {
- var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false };
- // get selector by context
- var ctx = selector.get(0).getContext("2d");
- // pointing parent container to make chart.js inherit its width
- var container = $(selector).parent();
- var generateChart = function() {
- selector.attr('width', $(container).width());
- if (window.innerWidth < 768) {
- // Scale fonts if window width lower than 768px (iPad portrait)
- options.scaleFontSize = 8
- }
- return new Chart(ctx).Bar(data, options);
- };
- // enabling auto-resizing
- $(window).resize(generateChart);
- return generateChart();
- };
-
- var chartData = function (keys, values) {
- var data = {
- labels : keys,
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : values
- }]
- };
- return data;
- };
-
- var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json});
- responsiveChart($('#hour-chart'), hourData);
-
- var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json});
- responsiveChart($('#weekday-chart'), dayData);
-
- var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json});
- responsiveChart($('#month-chart'), monthData);
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
deleted file mode 100644
index fcfcae0be20..00000000000
--- a/app/views/projects/graphs/languages.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-- @no_container = true
-- page_title "Languages", "Graphs"
-= render 'head'
-
-%div{ class: container_class }
- .sub-header-block
- .oneline
- Programming languages used in this repository
-
- .row
- .col-md-8
- %canvas#languages-chart{ height: 400 }
- .col-md-4
- %ul.bordered-list
- - @languages.each do |language|
- %li
- %span{ style: "color: #{language[:color]}" }
- = icon('circle')
- &nbsp;
- = language[:label]
- .pull-right
- = language[:value]
- \%
-
-:javascript
- var data = #{@languages.to_json};
- var ctx = $("#languages-chart").get(0).getContext("2d");
- var options = {
- scaleOverlay: true,
- responsive: true,
- maintainAspectRatio: false
- }
- var myPieChart = new Chart(ctx).Pie(data, options);
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 5ebb939a109..680f8ae6c8f 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,6 +1,9 @@
- @no_container = true
-- page_title "Contributors", "Graphs"
-= render 'head'
+- page_title "Contributors"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('graphs')
+= render 'projects/commits/head'
%div{ class: container_class }
.sub-header-block
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 7076f5db015..8b011af78eb 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,8 +1,2 @@
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
-
-:javascript
- $('.assign-to-me-link').on('click', function(e){
- $('#issue_assignee_id').val("#{current_user.id}").trigger("change");
- e.preventDefault();
- });
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 4825820c4d9..7a188cb6445 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -7,7 +7,7 @@
= nav_link(controller: :issues) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
%span
- Issues
+ List
= nav_link(controller: :boards) do
= link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index a0df0db77c5..4feec09bb5d 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
+ xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any?
xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any?
end
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 8ea1a3a45e1..f3a429d12d9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -10,26 +10,23 @@
= page_specific_javascript_bundle_tag('filtered_search')
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
+ = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
- if project_issues(@project).exists?
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- - if current_user
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
- = icon('rss')
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace,
- @project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New Issue",
- id: "new_issue_link" do
- New Issue
+ = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
+ = icon('rss')
+ = link_to new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New Issue",
+ id: "new_issue_link" do
+ New Issue
= render 'shared/issuable/search_bar', type: :issues
.issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 069f3d97943..6ac05bf3afe 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,8 +2,6 @@
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_vue')
.clearfix.detail-page-header
.issuable-header
@@ -22,37 +20,34 @@
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue")
- - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
- .issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
- %ul
- - if can?(current_user, :create_issue, @project)
- %li
- = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- - if can?(current_user, :update_issue, @issue)
- %li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- %li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- %li
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- - if @issue.submittable_as_spam_by?(current_user)
- %li
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
- New issue
- - if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ %li
+ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+ - if can?(current_user, :update_issue, @issue)
+ %li
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ %li
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ %li
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ New issue
+ - if can?(current_user, :update_issue, @issue)
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if @issue.submittable_as_spam_by?(current_user)
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
index 09aa401e44a..6da7c317f3a 100644
--- a/app/views/projects/issues/verify.html.haml
+++ b/app/views/projects/issues/verify.html.haml
@@ -1,4 +1,5 @@
- form = [@project.namespace.becomes(Namespace), @project, @issue]
= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
- = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+ = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
+ = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index a80f9aa4c4a..04bd4e8b683 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -2,16 +2,15 @@
This service will be installed on the Mattermost instance at
%strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
%hr
-= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f|
%h4 Team
%p
= @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in
- - selected_id = @teams.one? ? @teams.keys.first : 0
- - options = mattermost_teams_options(@teams)
- - options = options_for_select(options, selected_id)
- = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
- = f.hidden_field(:team_id, value: selected_id) if @teams.one?
+ - selected_id = @teams.one? ? @teams.first['id'] : nil
+ - options = options_for_select(mattermost_teams_options(@teams), selected_id)
+ = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
+ = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
.help-block
- if @teams.one?
This is the only available team.
@@ -25,7 +24,7 @@
%hr
%h4 Command trigger word
%p Choose the word that will trigger commands
- = f.text_field(:trigger, value: @project.path, class: 'form-control')
+ = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
.help-block
%p
Trigger word must be unique, and can't begin with a slash or contain any spaces.
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 96b1d2aee61..15829a3f143 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,3 +1,5 @@
+- @body_class = 'card-content'
+
.service-installation
.inline.pull-right
= custom_icon('mattermost_logo', size: 48)
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 88525f4036a..9607a7b5d06 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,8 +1,2 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
-
-:javascript
- $('.assign-to-me-link').on('click', function(e){
- $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
- e.preventDefault();
- });
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 466ec1475d8..ad14b4e583e 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
@@ -30,7 +30,7 @@
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer
- = icon('spinner spin', class: 'js-source-loading')
+ .text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
.col-md-6
@@ -60,7 +60,7 @@
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer
- = icon('spinner spin', class: "js-target-loading")
+ .text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index bd72310c16b..e7fcac4c477 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -52,11 +52,6 @@
= spinner
:javascript
- $('.assign-to-me-link').on('click', function(e){
- $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
- e.preventDefault();
- });
-:javascript
var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 521b0694ca9..6682a85ffa6 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,6 +3,7 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('diff_notes')
.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
@@ -15,7 +16,7 @@
.pull-right
- if @merge_request.source_branch_exists?
- if koding_enabled? && @repository.koding_yml
- = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
@@ -28,9 +29,9 @@
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
- %span Request to merge
+ %span <b>Request to merge</b>
%span.label-branch= source_branch_with_namespace(@merge_request)
- %span into
+ %span <b>into</b>
%span.label-branch
= link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
@@ -81,6 +82,7 @@
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
= render "discussions/jump_to_next"
.tab-content#diff-notes-app
diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
index eab5be488b5..eab5be488b5 100644
--- a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml
+++ b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index 1ecd9924d88..51d59280be8 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,6 +1,6 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_vue')
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 83e6c026ba7..8a96c8dacf6 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,7 +2,6 @@
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
- page_title "Merge Requests"
-= render "projects/issues/head"
= render 'projects/last_push'
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index 84b6c9ebc5c..f0a23bec5e7 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -2,9 +2,9 @@
- when :success
:plain
merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
-- when :merge_when_build_succeeds
+- when :merge_when_pipeline_succeeds
:plain
- $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}");
+ $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
- when :sha_mismatch
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 93ed4b68e0e..cde0ce08e14 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index c676953f729..1298376ac25 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,8 +1,8 @@
- if @pipeline
.mr-widget-heading
- - %w[success success_with_warnings skipped canceled failed running pending].each do |status|
+ - %w[success success_with_warnings skipped manual canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
- %div{ class: "ci-status-icon-#{status}" }
+ %div{ class: "ci-status-icon ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status)
%span
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 7794d6d7df2..adc3bbc37f3 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -7,28 +7,46 @@
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
- %p
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- The source branch has been removed.
+ .remove-message-pipes
+ %ul
+ %li
+ %span
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ %li
+ %span
+ The source branch has been removed.
= render 'projects/merge_requests/widget/merged_buttons'
- elsif @merge_request.can_remove_source_branch?(current_user)
- .remove_source_branch_widget
- %p
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- You can remove the source branch now.
+ .remove_source_branch_widget.remove-message-pipes
+ %ul
+ %li
+ %span
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ %li
+ %span
+ You can remove the source branch now.
= render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
- .remove_source_branch_widget.failed.hide
- %p
- Failed to remove source branch '#{@merge_request.source_branch}'.
-
- .remove_source_branch_in_progress.hide
- %p
- = icon('spinner spin')
- Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
+ .remove_source_branch_widget.failed.remove-message-pipes.hide
+ %ul
+ %li
+ %span
+ Failed to remove source branch '#{@merge_request.source_branch}'.
+ .remove_source_branch_in_progress.remove-message-pipes.hide
+ %ul
+ %li
+ %span
+ = icon('spinner spin')
+ Removing source branch '#{@merge_request.source_branch}'.
+ %li
+ %span
+ Please wait, this page will be automatically reloaded.
- else
- %p
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- = render 'projects/merge_requests/widget/merged_buttons'
+ .remove-message-pipes
+ %ul
+ %li
+ %span
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 9eef011b591..caf3bf54eef 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -9,6 +9,6 @@
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
- = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close")
- if mr_can_be_cherry_picked
= cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index c0d6ab669b8..bc426f1dc0c 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -19,14 +19,16 @@
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
- - elsif @merge_request.merge_when_build_succeeds?
- = render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
+ - elsif @merge_request.merge_when_pipeline_succeeds?
+ = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
- elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?)
= render 'projects/merge_requests/widget/open/build_failed'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
+ - elsif @pipeline&.blocked?
+ = render 'projects/merge_requests/widget/open/manual'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index b730ced4214..c94c7944c0b 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,8 +1,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('merge_request_widget')
-- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
-
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
= hidden_field_tag :sha, @merge_request.diff_head_sha
@@ -11,24 +9,24 @@
.accept-action
- if @pipeline && @pipeline.active?
%span.btn-group
- = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
+ = button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do
Merge When Pipeline Succeeds
- - unless @project.only_allow_merge_if_build_succeeds?
- = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
+ - unless @project.only_allow_merge_if_pipeline_succeeds?
+ = button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do
= icon('caret-down')
%span.sr-only
Select Merge Moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li
- = link_to "#", class: "merge_when_build_succeeds" do
+ = link_to "#", class: "merge_when_pipeline_succeeds" do
= icon('check fw')
Merge When Pipeline Succeeds
%li
- = link_to "#", class: "accept_merge_request" do
+ = link_to "#", class: "accept-merge-request" do
= icon('warning fw')
Merge Immediately
- else
- = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do
+ = f.button class: "btn btn-grouped js-merge-button accept-merge-request" do
Accept Merge Request
- if @merge_request.force_remove_source_branch?
.accept-control
@@ -49,4 +47,4 @@
text: @merge_request.merge_commit_message,
rows: 14, hint: true
- = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
+ = hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off"
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index c98b2c42597..621ee313026 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -3,20 +3,24 @@
- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
%h4.has-conflicts
- = icon("exclamation-triangle")
- This merge request contains merge conflicts
+ %p
+ = icon("exclamation-triangle")
+ This merge request contains merge conflicts
-%p
- To merge this request, resolve these conflicts
- - if can_resolve && !can_resolve_in_ui
- locally
- or
- - unless can_merge
- ask someone with write access to this repository to
- merge it locally.
+.remove-message-pipes
+ %ul
+ %li
+ %span
+ To merge this request, resolve these conflicts
+ - if can_resolve && !can_resolve_in_ui
+ locally
+ or
+ - unless can_merge
+ ask someone with write access to this repository to
+ merge it locally.
- if (can_resolve && can_resolve_in_ui) || can_merge
- .btn-group
+ .merged-buttons.clearfix
- if can_resolve && can_resolve_in_ui
= link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
- if can_merge
diff --git a/app/views/projects/merge_requests/widget/open/_manual.html.haml b/app/views/projects/merge_requests/widget/open/_manual.html.haml
new file mode 100644
index 00000000000..9078b7e21dd
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_manual.html.haml
@@ -0,0 +1,4 @@
+%h4
+ Pipeline blocked
+%p
+ The pipeline for this merge request requires a manual action to proceed.
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
deleted file mode 100644
index cf7abf3756c..00000000000
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('merge_request_widget')
-
-%h4
- Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
- to be merged automatically when the pipeline succeeds.
-%div
- %p
- = succeed '.' do
- The changes will be merged into
- %span.label-branch= @merge_request.target_branch
- - if @merge_request.remove_source_branch?
- The source branch will be removed.
- - else
- The source branch will not be removed.
-
- - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
- - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- - if remove_source_branch_button || user_can_cancel_automatic_merge
- .clearfix.prepend-top-10
- - if remove_source_branch_button
- = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
- = icon('times')
- Remove Source Branch When Merged
-
- - if user_can_cancel_automatic_merge
- = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
- Cancel Automatic Merge
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml
new file mode 100644
index 00000000000..5f347acce4d
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml
@@ -0,0 +1,33 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('merge_request_widget')
+
+%h4
+ Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
+ to be merged automatically when the pipeline succeeds.
+.remove-message-pipes
+ %ul
+ %li
+ %span
+ = succeed '.' do
+ The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}
+ - if @merge_request.remove_source_branch?
+ %li
+ %span
+ The source branch will be removed.
+ - else
+ %li
+ %span
+ The source branch will not be removed.
+
+ - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
+ - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+ - if remove_source_branch_button || user_can_cancel_automatic_merge
+ .clearfix.prepend-top-10
+ - if remove_source_branch_button
+ = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
+ = icon('times')
+ Remove Source Branch When Merged
+
+ - if user_can_cancel_automatic_merge
+ = link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
+ Cancel Automatic Merge
diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
index e094f97f3b6..ec9346ce89b 100644
--- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -6,5 +6,5 @@
Please resolve these discussions
- if @project.issues_enabled? && can?(current_user, :create_issue, @project)
or
- = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid)
+ = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: @merge_request.iid)
to allow this merge request to be merged.
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 11f41e75e63..55b0b837c6d 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title
- Edit Milestone ##{@milestone.iid}
+ Edit Milestone #{@milestone.to_reference}
%hr
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index ad2bfbec915..918f5d161bb 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,14 +1,14 @@
- @no_container = true
-- page_title "Milestones"
-= render "projects/issues/head"
+- page_title 'Milestones'
+= render 'projects/issues/head'
%div{ class: container_class }
.top-area
- = render 'shared/milestones_filter'
+ = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
- if can?(current_user, :admin_milestone, @project)
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone
.milestones
@@ -19,4 +19,4 @@
%li
.nothing-here-block No milestones to show
- = paginate @milestones, theme: "gitlab"
+ = paginate @milestones, theme: 'gitlab'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 06a31698ee6..d16f49bd33a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -19,10 +19,9 @@
Open
.header-text-content
%span.identifier
- Milestone ##{@milestone.iid}
+ %strong
+ Milestone #{@milestone.to_reference}
- if @milestone.due_date || @milestone.start_date
- %span.creator
- &middot;
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
@@ -47,7 +46,7 @@
= preserve do
= markdown_field(@milestone, :description)
- - if @milestone.total_items_count(current_user).zero?
+ - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
%span Assign some issues to this milestone.
- elsif @milestone.complete?(current_user) && @milestone.active?
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b88eef65cef..ed6077f6c6b 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,6 +1,5 @@
-- page_title "Network", @ref
+- page_title "Graph", @ref
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/raphael.js')
= page_specific_javascript_bundle_tag('network')
= render "projects/commits/head"
= render "head"
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 2a98bba05ee..d129da943f8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,5 +1,6 @@
- page_title 'New Project'
- header_title "Projects", dashboard_projects_path
+- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
.project-edit-container
.project-edit-errors
@@ -95,7 +96,7 @@
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
- = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index a73e8f345e0..5552086bc50 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
+%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
.timeline-entry-inner
.timeline-icon
%a{ href: user_path(note.author) }
@@ -30,27 +30,30 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
+ %resolve-btn{ "project-path" => project_path(note.project),
+ "discussion-id" => note.discussion_id,
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
- "resolved-by" => "#{note.resolved_by.try(:name)}",
+ ":author-name" => "'#{j(note.author.name)}'",
+ "author-avatar" => note.author.avatar_url,
+ ":note-truncated" => "'#{truncate(note.note, length: 17)}'",
+ ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
- .note-action-button
+ %button.note-action-button.line-resolve-btn{ type: "button",
+ class: ("is-disabled" unless can_resolve),
+ ":class" => "{ 'is-active': isResolved }",
+ ":aria-label" => "buttonText",
+ "@click" => "resolve",
+ ":title" => "buttonText",
+ "v-show" => "!loading",
+ ":ref" => "'button'" }
= icon("spin spinner", "v-show" => "loading")
- %button.line-resolve-btn{ type: "button",
- class: ("is-disabled" unless can_resolve),
- ":class" => "{ 'is-active': isResolved }",
- ":aria-label" => "buttonText",
- "@click" => "resolve",
- ":title" => "buttonText",
- "v-show" => "!loading",
- ":ref" => "'button'" }
- = render "shared/icons/icon_status_success.svg"
+ = render "shared/icons/icon_status_success.svg"
- if current_user
- if note.emoji_awardable?
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 08c73d94a09..90a150aa74c 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -23,4 +23,4 @@
to post a comment
:javascript
- var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+ var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
index 9db46f0b1fc..e442e6e9a09 100644
--- a/app/views/projects/pages/_use.html.haml
+++ b/app/views/projects/pages/_use.html.haml
@@ -5,4 +5,6 @@
.panel-body
%p
Learn how to upload your static site and have it served by
- GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
+ GitLab by following the
+ = succeed '.' do
+ = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank'
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index b6595269b06..259d5bd63d6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,4 +1,6 @@
- page_title 'Pages'
+= render "projects/settings/head"
+
%h3.page_title
Pages
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index 721a9b6beb5..a5acb7ac4a5 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -4,25 +4,25 @@
.nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
+ = nav_link(path: 'pipelines#index', controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
- if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
+ = nav_link(path: 'builds#index', controller: :builds) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
- if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
+ = nav_link(path: 'environments#index', controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
- - if can?(current_user, :read_cycle_analytics, @project)
- = nav_link(controller: %w(cycle_analytics)) do
- = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
+ = nav_link(path: 'pipelines#charts') do
+ = link_to charts_namespace_project_pipelines_path(@project.namespace, @project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span
- Cycle Analytics
+ Charts
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
index a0b14a7274a..3feb99cfcd7 100644
--- a/app/views/projects/pipelines/_stage.html.haml
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -1,3 +1,5 @@
-- @stage.statuses.latest.each do |status|
- %li
- = render 'ci/status/dropdown_graph_badge', subject: status
+- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status)
+- HasStatus::ORDERED_STATUSES.each do |ordered_status|
+ - grouped_statuses.fetch(ordered_status, []).each do |status|
+ %li
+ = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
new file mode 100644
index 00000000000..4a5043aac3c
--- /dev/null
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -0,0 +1,21 @@
+- @no_container = true
+- page_title "Charts", "Pipelines"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('graphs')
+= render 'head'
+
+%div{ class: container_class }
+ .sub-header-block
+ .oneline
+ A collection of graphs for Continuous Integration
+
+ #charts.ci-charts
+ .row
+ .col-md-6
+ = render 'projects/pipelines/charts/overall'
+ .col-md-6
+ = render 'projects/pipelines/charts/build_times'
+
+ %hr
+ = render 'projects/pipelines/charts/builds'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/pipelines/charts/_build_times.haml
index bb0975a9535..bb0975a9535 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/pipelines/charts/_build_times.haml
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/pipelines/charts/_builds.haml
index b6f453b9736..b6f453b9736 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/pipelines/charts/_builds.haml
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml
index edc4f7b079f..edc4f7b079f 100644
--- a/app/views/projects/graphs/ci/_overall.haml
+++ b/app/views/projects/pipelines/charts/_overall.haml
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 6e0428e2a31..5d59ce06612 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,35 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{ class: active_when(@scope.nil?) }>
+ %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
- %li{ class: active_when(@scope == 'running') }>
+ %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
+ = link_to project_pipelines_path(@project, scope: :pending) do
+ Pending
+ %span.badge
+ = number_with_delimiter(@pending_count)
+
+ %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
- = number_with_delimiter(@running_or_pending_count)
+ = number_with_delimiter(@running_count)
+
+ %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
+ = link_to project_pipelines_path(@project, scope: :finished) do
+ Finished
+ %span.badge
+ = number_with_delimiter(@finished_count)
- %li{ class: active_when(@scope == 'branches') }>
+ %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{ class: active_when(@scope == 'tags') }>
+ %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
@@ -36,28 +48,7 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
.content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
- "icon_status_canceled" => custom_icon("icon_status_canceled"),
- "icon_status_running" => custom_icon("icon_status_running"),
- "icon_status_skipped" => custom_icon("icon_status_skipped"),
- "icon_status_created" => custom_icon("icon_status_created"),
- "icon_status_pending" => custom_icon("icon_status_pending"),
- "icon_status_success" => custom_icon("icon_status_success"),
- "icon_status_failed" => custom_icon("icon_status_failed"),
- "icon_status_warning" => custom_icon("icon_status_warning"),
- "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
- "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
- "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
- "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
- "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
- "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
- "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
- "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
- "icon_play" => custom_icon("icon_play"),
- "icon_timer" => custom_icon("icon_timer"),
- "icon_status_manual" => custom_icon("icon_status_manual"),
- } }
-
- .vue-pipelines-index
+ .vue-pipelines-index
+= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 55202725b9e..14a270a3039 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,11 @@
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
- = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
+ = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
+ = dropdown_tag(params[:ref] || @project.default_branch,
+ options: { toggle_class: 'js-branch-select wide',
+ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+ data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 04b19a8c5a7..cf0db943865 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -23,6 +23,6 @@
- if can_admin_project
%th
%tbody
- = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
+ = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
= paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index e95a3b1b4c3..b8e885b4d9a 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -10,7 +10,7 @@
= f.label :name, class: 'col-md-2 text-right' do
Branch:
.col-md-10
- = render partial: "dropdown", locals: { f: f }
+ = render partial: "projects/protected_branches/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
such as
diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml
new file mode 100644
index 00000000000..2d8c519c025
--- /dev/null
+++ b/app/views/projects/protected_branches/_index.html.haml
@@ -0,0 +1,21 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('protected_branches')
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Protected Branches
+ %p Keep stable branches secure and force developers to use merge requests.
+ %p.prepend-top-20
+ By default, protected branches are designed to:
+ %ul
+ %li prevent their creation, if not already created, from everybody except Masters
+ %li prevent pushes from everybody except Masters
+ %li prevent <strong>anyone</strong> from force pushing to the branch
+ %li prevent <strong>anyone</strong> from deleting the branch
+ %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
+ .col-lg-9
+ - if can? current_user, :admin_project, @project
+ = render 'projects/protected_branches/create_protected_branch'
+
+ = render "projects/protected_branches/branches_list"
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0193800dedf..b2a6b8469a3 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -14,7 +14,7 @@
- else
(branch was removed from repository)
- = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+ = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch }
- if can_admin_project
%td
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
deleted file mode 100644
index b3b419bd92d..00000000000
--- a/app/views/projects/protected_branches/index.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-- page_title "Protected branches"
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('protected_branches')
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
- = page_title
- %p Keep stable branches secure and force developers to use merge requests.
- %p.prepend-top-20
- By default, protected branches are designed to:
- %ul
- %li prevent their creation, if not already created, from everybody except Masters
- %li prevent pushes from everybody except Masters
- %li prevent <strong>anyone</strong> from force pushing to the branch
- %li prevent <strong>anyone</strong> from deleting the branch
- %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
- .col-lg-9
- - if can? current_user, :admin_project, @project
- = render 'create_protected_branch'
-
- = render "branches_list"
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 3a323d94cc2..2fb88297fb3 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -4,13 +4,13 @@
%ul.list-unstyled.indent-list
%li
1.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
= icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
= icon('external-link')
in your Mattermost team with these options:
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index a04fd5035a6..2a1b9d4c465 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 0d973a20d4c..078b7be6865 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
@@ -57,7 +57,7 @@
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
new file mode 100644
index 00000000000..88bcb541dac
--- /dev/null
+++ b/app/views/projects/settings/_head.html.haml
@@ -0,0 +1,33 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if can_edit
+ = nav_link(controller: :projects) do
+ = link_to edit_project_path(@project), title: 'General' do
+ %span
+ General
+ = nav_link(controller: :members) do
+ = link_to project_settings_members_path(@project), title: 'Members' do
+ %span
+ Members
+ - if can_edit
+ = nav_link(controller: :integrations) do
+ = link_to project_settings_integrations_path(@project), title: 'Integrations' do
+ %span
+ Integrations
+ = nav_link(controller: :repository) do
+ = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
+ %span
+ Repository
+ - if @project.feature_available?(:builds, current_user)
+ = nav_link(controller: :ci_cd) do
+ = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+ %span
+ CI/CD Pipelines
+ = nav_link(controller: :pages) do
+ = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
+ %span
+ Pages
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 52f5f7b81e2..e2603096014 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,4 +1,5 @@
- page_title "CI/CD Pipelines"
+= render "projects/settings/head"
= render 'projects/runners/index'
= render 'projects/variables/index'
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index aa38a889cdd..f69992566b5 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,3 +1,4 @@
- page_title 'Integrations'
+= render "projects/settings/head"
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index d81ed7bb609..20e1ad68244 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Members"
+= render "projects/settings/head"
= render "projects/project_members/index"
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
new file mode 100644
index 00000000000..4c02302e161
--- /dev/null
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -0,0 +1,5 @@
+- page_title "Repository"
+= render "projects/settings/head"
+
+= render @deploy_keys
+= render "projects/protected_branches/index"
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 11310d5e1e1..5c7f2e315f0 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -1,7 +1,7 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@project.name} activity"
- xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
xml.updated @events[0].updated_at.xmlschema if @events[0]
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f7419728719..de1229d58aa 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,80 +1,80 @@
- @no_container = true
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
+ = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, rss_url_options), title: "#{@project.name} activity")
= content_for :flash_message do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
= render 'shared/no_password'
-= render 'projects/last_push'
+= render "projects/head"
+= render "projects/last_push"
= render "home_panel"
- if current_user && can?(current_user, :download_code, @project)
- .project-stats-container{ class: container_class }
- %nav.project-stats
- %ul.nav
- %li
- = link_to project_files_path(@project) do
- Files (#{storage_counter(@project.statistics.total_repository_size)})
- %li
- = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
- %li
- = link_to namespace_project_branches_path(@project.namespace, @project) do
- #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
- %li
- = link_to namespace_project_tags_path(@project.namespace, @project) do
- #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
+ %nav.project-stats{ class: container_class }
+ %ul.nav
+ %li
+ = link_to project_files_path(@project) do
+ Files (#{storage_counter(@project.statistics.total_repository_size)})
+ %li
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+ #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
+ %li
+ = link_to namespace_project_branches_path(@project.namespace, @project) do
+ #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
+ %li
+ = link_to namespace_project_tags_path(@project.namespace, @project) do
+ #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- - if default_project_view != 'readme' && @repository.readme
- %li
- = link_to 'Readme', readme_path(@project)
+ - if default_project_view != 'readme' && @repository.readme
+ %li
+ = link_to 'Readme', readme_path(@project)
- - if @repository.changelog
- %li
- = link_to 'Changelog', changelog_path(@project)
+ - if @repository.changelog
+ %li
+ = link_to 'Changelog', changelog_path(@project)
- - if @repository.license_blob
- %li
- = link_to license_short_name(@project), license_path(@project)
+ - if @repository.license_blob
+ %li
+ = link_to license_short_name(@project), license_path(@project)
- - if @repository.contribution_guide
- %li
- = link_to 'Contribution guide', contribution_guide_path(@project)
+ - if @repository.contribution_guide
+ %li
+ = link_to 'Contribution guide', contribution_guide_path(@project)
- - if @repository.gitlab_ci_yml
- %li
- = link_to 'CI configuration', ci_configuration_path(@project)
+ - if @repository.gitlab_ci_yml
+ %li
+ = link_to 'CI configuration', ci_configuration_path(@project)
- - if current_user && can_push_branch?(@project, @project.default_branch)
- - unless @repository.changelog
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
- Add Changelog
- - unless @repository.license_blob
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'LICENSE') do
- Add License
- - unless @repository.contribution_guide
- %li.missing
- = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
- Add Contribution guide
- - unless @repository.gitlab_ci_yml
- %li.missing
- = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
- Set up CI
- - if koding_enabled? && @repository.koding_yml.blank?
- %li.missing
- = link_to 'Set up Koding', add_koding_stack_path(@project)
- - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
- %li.missing
- = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
- Set up auto deploy
+ - if current_user && can_push_branch?(@project, @project.default_branch)
+ - unless @repository.changelog
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
+ Add Changelog
+ - unless @repository.license_blob
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'LICENSE') do
+ Add License
+ - unless @repository.contribution_guide
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
+ Add Contribution guide
+ - unless @repository.gitlab_ci_yml
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+ Set up CI
+ - if koding_enabled? && @repository.koding_yml.blank?
+ %li.missing
+ = link_to 'Set up Koding', add_koding_stack_path(@project)
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+ Set up auto deploy
- - if @repository.commit
+ - if @repository.commit
+ %div{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 6b3d7d4008b..e35385f4cab 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,13 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- .js-file-title.file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
- = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
+ = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 8ef069b9e05..dffe908e85a 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -23,7 +23,7 @@
= markdown_field(release, :description)
.row-fixed-content.controls
- = render 'projects/buttons/download', project: @project, ref: tag.name
+ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
index e4a78fadbeb..cde23e03d54 100644
--- a/app/views/projects/tags/destroy.js.haml
+++ b/app/views/projects/tags/destroy.js.haml
@@ -1,2 +1,4 @@
-- if @repository.tags.empty?
+- if @error.present?
+ new Flash('#{escape_javascript(@error)}', 'alert');
+- elsif @repository.tags.empty?
$('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index e2f132f7742..7f9a44e565f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/commits/head"
.flex-list{ class: container_class }
- .top-area.flex-row
+ .top-area.adjust
.nav-text.row-main-content
Tags give the ability to mark specific points in history as being important
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 9864be3562a..a2a26039220 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -2,8 +2,7 @@
- page_title @path.presence || "Files", @ref
= content_for :meta_tags do
- - if current_user
- = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= render "projects/commits/head"
= render 'projects/last_push'
diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
new file mode 100644
index 00000000000..ea32eac2ae2
--- /dev/null
+++ b/app/views/projects/triggers/_content.html.haml
@@ -0,0 +1,14 @@
+%h4.prepend-top-0
+ Triggers
+%p.prepend-top-20
+ Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
+ impersonate their associated user including their access to projects and their project
+ permissions.
+%p.prepend-top-20
+ Triggers with the
+ %span.label.label-primary legacy
+ label do not have an associated user and only have access to the current project.
+%p.append-bottom-0
+ = succeed '.' do
+ Learn more in the
+ = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
new file mode 100644
index 00000000000..5f708b3a2ed
--- /dev/null
+++ b/app/views/projects/triggers/_form.html.haml
@@ -0,0 +1,11 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f|
+ = form_errors(@trigger)
+
+ - if @trigger.token
+ .form-group
+ %label.label-light Token
+ %p.form-control-static= @trigger.token
+ .form-group
+ = f.label :key, "Description", class: "label-light"
+ = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
+ = f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index 33883facf9b..cc74e50a5e3 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,35 +1,31 @@
-.row.prepend-top-default.append-bottom-default
+.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-3
- %h4.prepend-top-0
- Triggers
- %p.prepend-top-20
- Triggers can force a specific branch or tag to get rebuilt with an API call.
- %p.append-bottom-0
- = succeed '.' do
- Learn more in the
- = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
+ = render "projects/triggers/content"
.col-lg-9
.panel.panel-default
.panel-heading
%h4.panel-title
Manage your project's triggers
.panel-body
+ = render "projects/triggers/form", btn_text: "Add trigger"
+ %hr
- if @triggers.any?
- .table-responsive
+ .table-responsive.triggers-list
%table.table
%thead
%th
%strong Token
%th
+ %strong Description
+ %th
+ %strong Owner
+ %th
%strong Last used
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
- No triggers have been created yet. Add one using the button below.
-
- = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
- = f.submit "Add trigger", class: 'btn btn-success'
+ No triggers have been created yet. Add one using the form above.
.panel-footer
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 112b51712ef..ed68e0ed56d 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -1,12 +1,42 @@
%tr
%td
- %span.monospace= trigger.token
+ - if can?(current_user, :admin_trigger, trigger)
+ %span= trigger.token
+ = clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard")
+ - else
+ %span= trigger.short_token
+
+ .label-container
+ - if trigger.legacy?
+ %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
+ - if !trigger.can_access_project?
+ %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
+
+ %td
+ - if trigger.description? && trigger.description.length > 15
+ %span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15)
+ - else
+ = trigger.description
+
+ %td
+ - if trigger.owner
+ .trigger-owner.sr-only= trigger.owner.name
+ = user_avatar(user: trigger.owner, size: 20)
%td
- - if trigger.last_trigger_request
- #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
+ - if trigger.last_used
+ #{time_ago_in_words(trigger.last_used)} ago
- else
Never
- %td.text-right
- = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm"
+ %td.text-right.trigger-actions
+ - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?"
+ - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
+ - if trigger.owner != current_user && can?(current_user, :manage_trigger, trigger)
+ = link_to 'Take ownership', take_ownership_namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: take_ownership_confirmation }, method: :post, class: "btn btn-default btn-sm btn-trigger-take-ownership"
+ - if can?(current_user, :admin_trigger, trigger)
+ = link_to edit_namespace_project_trigger_path(@project.namespace, @project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do
+ %i.fa.fa-pencil
+ - if can?(current_user, :manage_trigger, trigger)
+ = link_to namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
+ %i.fa.fa-trash
diff --git a/app/views/projects/triggers/edit.html.haml b/app/views/projects/triggers/edit.html.haml
new file mode 100644
index 00000000000..c35df322b9d
--- /dev/null
+++ b/app/views/projects/triggers/edit.html.haml
@@ -0,0 +1,9 @@
+- page_title "Trigger"
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "content"
+ .col-lg-9
+ %h4.prepend-top-0
+ Update trigger
+ = render "form", btn_text: "Save trigger"
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 763c2fea39b..5211ade1a5f 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -4,6 +4,6 @@
New Page
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page History
- - if can?(current_user, :create_wiki, @project)
+ - if can?(current_user, :create_wiki, @project) && @page.latest?
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
Edit
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 22004ecacbc..02133d09cdf 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -11,7 +11,7 @@
.results.prepend-top-10
- if @scope == 'commits'
- %ul.content-list.commit-list.table-list.table-wide
+ %ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects
- else
.search-results
diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml
new file mode 100644
index 00000000000..7799aff6b5b
--- /dev/null
+++ b/app/views/shared/_branch_switcher.html.haml
@@ -0,0 +1,8 @@
+- dropdown_toggle_text = @target_branch || tree_edit_branch
+= hidden_field_tag 'target_branch', dropdown_toggle_text
+
+.dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' }
+ .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches
+ = render partial: 'shared/projects/blob/branch_page_default'
+ = render partial: 'shared/projects/blob/branch_page_create'
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index efb207b9916..c2d9ac87b20 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -17,8 +17,9 @@
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
- pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
- title: 'Please choose a group name with no special characters.'
+ pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
+ title: 'Please choose a group name with no special characters.',
+ "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent
= f.hidden_field :parent_id, value: parent.id
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 1264e524d86..1d4fd71522d 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -2,6 +2,12 @@
- issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
+- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
+
+- if issuable_mr > 0
+ %li
+ = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
+ = issuable_mr
- if upvotes > 0
%li
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 1744a597c51..bd994cdad01 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -45,11 +45,11 @@
- if current_user && defined?(@project)
.label-subscription.inline
- if label.is_a?(ProjectLabel)
- %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
%span= label_subscription_toggle_button_text(label, @project)
= icon('spinner spin', class: 'label-subscribe-button-loading')
- else
- %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } }
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
%span Unsubscribe
= icon('spinner spin', class: 'label-subscribe-button-loading')
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 9b67422da2c..10e6c49ae9f 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg width="36" height="36" class="tanuki-logo">
+<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36">
<path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
<path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
<path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 704893b4d5b..57a0eaa919e 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,19 +1,13 @@
-- if @project
- - counts = milestone_counts(@project.milestones)
-
%ul.nav-links
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- - if @project
- %span.badge= counts[:opened]
+ %span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- - if @project
- %span.badge= counts[:closed]
+ %span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- - if @project
- %span.badge= counts[:all]
+ %span.badge= counts[:all]
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 0c8ac48bb58..3ac5e15d1c4 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,7 +7,7 @@
.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"
+ = render 'shared/branch_switcher'
.js-create-merge-request-container
.checkbox
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
new file mode 100644
index 00000000000..af4cc90f4a7
--- /dev/null
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -0,0 +1,39 @@
+- type = impersonation ? "Impersonation" : "Personal Access"
+
+%h5.prepend-top-0
+ Add a #{type} Token
+%p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique #{type} Token.
+
+= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control"
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
+
+ .prepend-top-default
+ = f.submit "Create #{type} Token", class: "btn btn-create"
+
+:javascript
+ var $dateField = $('.datepicker');
+ var date = $dateField.val();
+
+ new Pikaday({
+ field: $dateField.get(0),
+ theme: 'gitlab-theme',
+ format: 'yyyy-mm-dd',
+ minDate: new Date(),
+ onSelect: function(dateText) {
+ $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
+ });
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
new file mode 100644
index 00000000000..67a49815478
--- /dev/null
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -0,0 +1,60 @@
+- type = impersonation ? "Impersonation" : "Personal Access"
+%hr
+
+%h5 Active #{type} Tokens (#{active_tokens.length})
+- if impersonation
+ %p.profile-settings-content
+ To see all the user's personal access tokens you must impersonate them first.
+
+- if active_tokens.present?
+ .table-responsive
+ %table.table.active-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th Scopes
+ - if impersonation
+ %th Token
+ %th
+ %tbody
+ - active_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires?
+ %span{ class: ('text-warning' if token.expires_soon?) }
+ In #{distance_of_time_in_words_to_now(token.expires_at)}
+ - else
+ %span.token-never-expires-label Never
+ %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
+ - if impersonation
+ %td.token-token-container
+ = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control"
+ = clipboard_button(clipboard_text: token.token)
+ - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
+ %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+- else
+ .settings-message.text-center
+ This user has no active #{type} Tokens.
+
+%hr
+
+%h5 Inactive #{type} Tokens (#{inactive_tokens.length})
+- if inactive_tokens.present?
+ .table-responsive
+ %table.table.inactive-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %tbody
+ - inactive_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+- else
+ .settings-message.text-center
+ This user has no inactive #{type} Tokens.
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 0ce0d759e86..a212c714826 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
@@ -10,6 +9,8 @@
%li
= link_to page_filter_path(sort: sort_value_priority, label: true) do
= sort_title_priority
+ = link_to page_filter_path(sort: sort_value_label_priority, label: true) do
+ = sort_title_label_priority
= link_to page_filter_path(sort: sort_value_recently_created, label: true) do
= sort_title_recently_created
= link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e2033654018..7a7e3d46796 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- - else
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
- %h4.text-center There are no issues to show.
+ %h4 There are no issues to show.
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index ba5c2dae09d..00fb77bdb3b 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -5,7 +5,7 @@
.col-xs-12.col-sm-6
.text-content
%h4 Labels can be applied to issues and merge requests to categorize them.
- %p You can also star label to make it a priority label.
+ %p You can also star a label to make it a priority label.
- if can?(current_user, :admin_label, @project)
= link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
= link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
new file mode 100644
index 00000000000..37589b634fa
--- /dev/null
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -0,0 +1,18 @@
+.dropdown.inline
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ = link_to filter_groups_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to filter_groups_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+ = link_to filter_groups_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to filter_groups_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 60ca23ef680..a95020a9be8 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,5 +1,6 @@
- group_member = local_assigns[:group_member]
- full_name = true unless local_assigns[:full_name] == false
+- group_name = full_name ? group.full_name : group.name
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
@@ -28,11 +29,7 @@
.avatar-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
- = link_to group, class: 'group-name' do
- - if full_name
- = group.full_name
- - else
- = group.name
+ = link_to group_name, group, class: 'group-name'
- if group_member
as
diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml
new file mode 100644
index 00000000000..ad7a7faedf1
--- /dev/null
+++ b/app/views/shared/groups/_search_form.html.haml
@@ -0,0 +1,2 @@
+= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f|
+ = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
diff --git a/app/views/shared/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb
new file mode 100644
index 00000000000..917753fb343
--- /dev/null
+++ b/app/views/shared/icons/_collapse.svg.erb
@@ -0,0 +1 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/views/shared/icons/_icon_customization.svg b/app/views/shared/icons/_icon_customization.svg
new file mode 100644
index 00000000000..eb1f8ba129b
--- /dev/null
+++ b/app/views/shared/icons/_icon_customization.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg
new file mode 100644
index 00000000000..d1c541523ab
--- /dev/null
+++ b/app/views/shared/icons/_icon_mattermost.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M250.05 34c1.9.04 3.8.11 5.6.2l-29.79 35.51c-.07.01-.15.03-.23.04C149.26 84.1 98.22 146.5 98.22 222.97c0 41.56 23.07 90.5 59.75 119.1 28.61 22.32 64.29 36.9 101.21 36.9 93.4 0 160.15-68.61 160.15-156 0-34.91-15.99-72.77-41.76-100.76l-1.63-47.39c54.45 39.15 89.95 103.02 90.06 175.17v.01c0 119.29-96.7 216-216 216-119.29 0-216-96.71-216-216S130.71 34 250 34h.05zm64.1 20.29c.66-.04 1.32.03 1.96.25 3.01 1 3.85 3.57 3.93 6.45l3.84 146.88c.76 28.66-17.16 68.44-60.39 68.56-30.97.08-63.68-20.83-63.68-60.13.01-14.73 5.61-31.26 19.25-48.11l90.03-111.18c1.15-1.42 3.08-2.58 5.06-2.72z"/></svg>
diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg
new file mode 100644
index 00000000000..ae219a3ded2
--- /dev/null
+++ b/app/views/shared/icons/_icon_mr_issue.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index f17ae9f28eb..847a86e2e68 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,4 @@
-- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
+- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
@@ -24,7 +24,7 @@
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
@@ -34,21 +34,7 @@
%a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
.pull-right
- - if boards_page
- #js-boards-search.issue-boards-search
- %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- - if can?(current_user, :admin_list, @project)
- #js-add-issues-btn.pull-right.prepend-left-10
- .dropdown.pull-right
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
- Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, @project)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
- - else
- = render 'shared/sort_dropdown'
+ = render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index cb92b2e97a7..17107f55a2d 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
.alert.alert-danger
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
- = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
.form-group
@@ -45,41 +45,47 @@
= render 'shared/issuable/form/merge_params', issuable: issuable
-- if @merge_request_for_resolving_discussions
+- if @merge_request_to_resolve_discussions_of
.form-group
.col-sm-10.col-sm-offset-2
- - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user)
- = icon('exclamation-triangle')
- Creating this issue will mark all discussions in
- = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
- as resolved.
- = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid
+ = icon('info-circle')
+ - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
+ = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
+ - if @discussion_to_resolve
+ = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id
+ Creating this issue will resolve the discussion in
+ - else
+ Creating this issue will resolve all discussions in
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
- else
- = icon('exclamation-triangle')
- You can't automatically mark all discussions in
- = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
- as resolved. Ask someone with sufficient rights to resolve the them.
+ The
+ = @discussion_to_resolve ? 'discussion' : 'discussions'
+ at
+ = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve)
+ will stay unresolved. Ask someone with permission to resolve
+ = @discussion_to_resolve ? 'it.' : 'them.'
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- - if issuable.new_record?
- = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- - else
- = form.submit 'Save changes', class: 'btn btn-save'
+ .pull-right
+ - if issuable.new_record?
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
+ - else
+ - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
+ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
+
+ %span.append-right-10
+ - if issuable.new_record?
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ - else
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
- .inline.prepend-left-10
+ .inline.prepend-top-10
Please review the
%strong= link_to('contribution guidelines', guide_url)
for this project.
- - if issuable.new_record?
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- - else
- .pull-right
- - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
- method: :delete, class: 'btn btn-danger btn-grouped'
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 415361f8fbf..f0d50828e2a 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -6,7 +6,7 @@
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 8e04b50bb8a..b58640c3ef0 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,7 +1,8 @@
- type = local_assigns.fetch(:type)
+- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
.issues-filters
- .issues-details-filters.row-content-block.second-block.filtered-search-block
+ .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
@@ -11,18 +12,21 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
- = icon('filter')
- %button.clear-search.hidden{ type: 'button' }
- = icon('times')
+ .scroll-container
+ %ul.tokens-container.list-unstyled
+ %li.input-token
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
+ = icon('filter')
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-action' => 'submit' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link
= icon('search')
%span
Keep typing and press Enter
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
@@ -33,57 +37,72 @@
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
- %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
- %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
.dropdown-user-details
%span
{{name}}
%span.dropdown-light-content
@{{username}}
#js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Milestone
- %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+ %li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link
Upcoming
+ %li.filter-dropdown-item{ 'data-value' => 'started' }
+ %button.btn.btn-link
+ Started
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{title}}
- #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
- %ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => 'none' }
+ #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } }
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Label
%li.divider
- %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
- .pull-right
- = render 'shared/sort_dropdown'
+ .filter-dropdown-container
+ - if type == :boards
+ - if can?(current_user, :admin_list, @project)
+ .dropdown.prepend-left-10#js-add-list
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
+ Add list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
+ #js-add-issues-btn.prepend-left-10
+ - elsif type != :boards_modal
+ = render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
@@ -112,22 +131,23 @@
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
- .filter-item.inline
+ .filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
-:javascript
- new UsersSelect();
- new LabelsSelect();
- new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+- unless type === :boards_modal
+ :javascript
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
- $(document).off('page:restore').on('page:restore', function (event) {
- if (gl.FilteredSearchManager) {
- new gl.FilteredSearchManager();
- }
- Issuable.init();
- new gl.IssuableBulkActions({
- prefixId: 'issue_',
+ $(document).off('page:restore').on('page:restore', function (event) {
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: 'issue_',
+ });
});
- });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3f7f1a86b9f..25a4aec0a38 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,6 @@
- todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issuable')
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
@@ -9,16 +10,16 @@
- if current_user
%span.issuable-header-text.hide-collapsed.pull-left
Todo
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
%span.js-issuable-todo-text
- if todo
Mark done
- else
Add todo
- = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
+ = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
.block.assignee
@@ -26,10 +27,10 @@
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
- = icon('user')
+ = icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -37,7 +38,7 @@
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
- = icon('exclamation-triangle')
+ = icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= issuable.assignee.to_reference
- else
@@ -54,7 +55,7 @@
.block.milestone
.sidebar-collapsed-icon
- = icon('clock-o')
+ = icon('clock-o', 'aria-hidden': 'true')
%span
- if issuable.milestone
%span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
@@ -63,7 +64,7 @@
None
.title.hide-collapsed
Milestone
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -77,20 +78,20 @@
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block
- %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
+ %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'docs-url' => help_page_path('workflow/time_tracking.md') }
// Fallback while content is loading
.title.hide-collapsed
Time tracking
- = icon('spinner spin')
+ = icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
.sidebar-collapsed-icon
- = icon('calendar')
+ = icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
.title.hide-collapsed
Due date
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -110,7 +111,7 @@
.dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
%span.dropdown-toggle-text Due date
- = icon('chevron-down')
+ = icon('chevron-down', 'aria-hidden': 'true')
.dropdown-menu.dropdown-menu-due-date
= dropdown_title('Due date')
= dropdown_content do
@@ -120,12 +121,12 @@
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
- = icon('tags')
+ = icon('tags', class: 'hidden', 'aria-hidden': 'true')
%span
= selected_labels.size
.title.hide-collapsed
Labels
- = icon('spinner spin', class: 'block-loading')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
@@ -141,7 +142,7 @@
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
- = icon('chevron-down')
+ = icon('chevron-down', 'aria-hidden': 'true')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
- if can? current_user, :admin_label, @project and @project
@@ -152,7 +153,7 @@
- subscribed = issuable.subscribed?(current_user, @project)
.block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
.sidebar-collapsed-icon
- = icon('rss')
+ = icon('rss', 'aria-hidden': 'true')
%span.issuable-header-text.hide-collapsed.pull-left
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
@@ -173,7 +174,7 @@
:javascript
gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
- new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
+ new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
gl.Subscription.bindAll('.subscription');
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index a47085230b8..9dbfedb84f1 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -13,15 +13,15 @@
= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- - if issuable.assignee_id
- = form.hidden_field :assignee_id
+ = form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
.form-group.issue-milestone
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- has_labels = @labels && @labels.any?
= form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index a93cbd1041f..8af3bd597c5 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -13,6 +13,6 @@
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
= render partial: 'shared/milestones/issuable',
- collection: issuables.sort_by(&:position),
+ collection: issuables.order_position_asc,
as: :issuable,
locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index d27fba805a3..78079f633d5 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -6,14 +6,15 @@
.milestone-stats-and-buttons
.milestone-stats
- %span.milestone-stat.with-drilldown
- %strong= milestone.issues_visible_to_user(current_user).size
- issues:
- %span.milestone-stat
- %strong= milestone.issues_visible_to_user(current_user).opened.size
- open and
- %strong= milestone.issues_visible_to_user(current_user).closed.size
- closed
+ - if !project || can?(current_user, :read_issue, project)
+ %span.milestone-stat.with-drilldown
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
%span.milestone-stat.with-drilldown
%strong= milestone.merge_requests.size
merge requests:
@@ -32,10 +33,12 @@
.milestone-progress-buttons
%span.tab-issues-buttons
- - if project && can?(current_user, :create_issue, project)
- = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
- New Issue
- = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
+ - if project
+ - if can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
+ New Issue
+ - if can?(current_user, :read_issue, project)
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
%span.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index c8f2319d95a..a0e9ec46220 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -1,12 +1,18 @@
%ul.nav-links.no-top.no-bottom
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Issues
- %span.badge= milestone.issues_visible_to_user(current_user).size
- %li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= milestone.merge_requests.size
+ - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).size
+ %li
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
+ - else
+ %li.active
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
@@ -20,10 +26,14 @@
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
.tab-content.milestone-content
- .tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
- .tab-pane#tab-merge-requests
- = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+ .tab-pane.active#tab-issues
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-merge-requests
+ = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ - else
+ .tab-pane.active#tab-merge-requests
+ = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants
.tab-pane#tab-labels
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index c19697802ce..2d25b8aad62 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,4 @@
- @sort ||= sort_value_recently_updated
-- personal = params[:personal]
-- archived = params[:archived]
-- shared = params[:shared]
-- namespace_id = params[:namespace_id]
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
@@ -11,32 +7,32 @@
Sort by
- projects_sort_options_hash.each do |value, title|
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do
+ = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do
= title
%li.divider
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do
+ = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do
Show archived projects
- if current_user
%li.divider
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do
+ = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do
Owned by anyone
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
+ = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do
Owned by me
- if @group && @group.shared_projects.present?
%li.divider
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do
+ = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
All projects
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do
+ = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
Hide shared projects
%li
- = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do
+ = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
Hide group projects
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 3a9dd37dc7d..c57282c5742 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -8,7 +8,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
-.projects-list-holder
+.js-projects-list-holder
- if projects.any?
%ul.projects-list.content-list
- projects.each_with_index do |project, i|
@@ -25,6 +25,3 @@
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.nothing-here-block No projects found
-
-:javascript
- ProjectsList.init();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4a27965754d..df21857e1ad 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -6,17 +6,16 @@
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(project.commit.status) if project.commit.try(:status)
+- cache_key = project_list_cache_key(project)
%li.project-row{ class: css_class }
= cache(cache_key) do
.controls
- if project.archived
%span.label.label-warning archived
- - if project.commit.try(:status)
+ - if project.pipeline_status.has_status?
%span
- = render_commit_status(project.commit)
+ = render_project_pipeline_status(project.pipeline_status)
- if forks
%span
= icon('code-fork')
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
new file mode 100644
index 00000000000..b89194bcc67
--- /dev/null
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -0,0 +1,23 @@
+= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = search_field_tag :name, params[:name],
+ placeholder: 'Filter by name...',
+ class: 'project-filter-form-field form-control input-short js-projects-list-filter',
+ spellcheck: false,
+ id: 'project-filter-form-field',
+ tabindex: "2",
+ autofocus: local_assigns[:autofocus]
+
+ - if local_assigns[:icon]
+ = icon("search", class: "search-icon")
+
+ - if params[:sort].present?
+ = hidden_field_tag :sort, params[:sort]
+
+ - if params[:personal].present?
+ = hidden_field_tag :personal, params[:personal]
+
+ - if params[:archived].present?
+ = hidden_field_tag :archived, params[:archived]
+
+ - if params[:visibility_level].present?
+ = hidden_field_tag :visibility_level, params[:visibility_level]
diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml
new file mode 100644
index 00000000000..c279a0d8846
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_create.html.haml
@@ -0,0 +1,8 @@
+.dropdown-page-two.dropdown-new-branch
+ = dropdown_title('Create new branch', back: true)
+ = dropdown_content do
+ %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml
new file mode 100644
index 00000000000..9bf78d10878
--- /dev/null
+++ b/app/views/shared/projects/blob/_branch_page_default.html.haml
@@ -0,0 +1,10 @@
+.dropdown-page-one
+ = dropdown_title "Select branch"
+ = dropdown_filter "Search branches"
+ = dropdown_content
+ = dropdown_loading
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{ href: "#" }
+ Create new branch
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index ad5c0c2d8c8..74f71e6cbd1 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,7 +1,25 @@
-- unless @snippet.content.empty?
+.js-file-title.file-title-flex-parent
+ .file-header-content
+ = blob_icon @snippet.mode, @snippet.path
+
+ %strong.file-title-name
+ = @snippet.path
+
+ = copy_file_path_button(@snippet.path)
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = copy_blob_content_button(@snippet)
+ = open_raw_file_button(raw_path)
+
+ - if defined?(download_path) && download_path
+ = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
+
+- if @snippet.content.empty?
+ .file-content.code
+ .nothing-here-block Empty file
+- else
- if markup?(@snippet.file_name)
- %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } }
- = @snippet.content
.file-content.wiki
- if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content))
@@ -9,6 +27,3 @@
= render_markup(@snippet.file_name, @snippet.content)
- else
= render 'shared/file_highlight', blob: @snippet
-- else
- .file-content.code
- .nothing-here-block Empty file
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index e7f7db73223..0296597b294 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('snippet')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
+ = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f|
= form_errors(@snippet)
.form-group
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 970afbe6b64..da9fb755a36 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,13 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- .js-file-title.file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
- = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
- = render 'shared/snippets/blob'
+ = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet)
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true
+.row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dc2fea450bd..dc9a3b0d0df 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,7 +1,7 @@
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('lib_d3')
+ = page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -24,17 +24,16 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- - if current_user
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
- - if current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('users')
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do
+ = icon('rss')
+ - if current_user && current_user.admin?
+ = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('users')
.profile-header
.avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
.user-info
@@ -98,6 +97,7 @@
Snippets
%div{ class: container_class }
+ .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 0e20df506a3..13207a8bc71 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -10,7 +10,7 @@ class AuthorizedProjectsWorker
end
def self.bulk_perform_async(args_list)
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
end
def perform(user_id)
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa0..00000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
- include Sidekiq::Worker
- include BuildQueue
-
- def perform(build_id, recipients, push_data)
- recipients.each do |recipient|
- begin
- case push_data['build_status']
- when 'success'
- Notify.build_success_email(build_id, recipient).deliver_now
- when 'failed'
- Notify.build_fail_email(build_id, recipient).deliver_now
- end
- # These are input errors and won't be corrected even if Sidekiq retries
- rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
- end
- end
- end
-end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 7e44b241743..c9658b3fe17 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -120,8 +120,8 @@ class IrkerWorker
end
def compare_url(data, repo_path)
- sha1 = Commit::truncate_sha(data['before'])
- sha2 = Commit::truncate_sha(data['after'])
+ sha1 = Commit.truncate_sha(data['before'])
+ sha2 = Commit.truncate_sha(data['after'])
compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
compare_url += "/#{sha1}...#{sha2}"
colorize_url compare_url
@@ -129,7 +129,7 @@ class IrkerWorker
def send_one_commit(project, hook_attrs, repo_name, branch)
commit = commit_from_id project, hook_attrs['id']
- sha = colorize_sha Commit::truncate_sha(hook_attrs['id'])
+ sha = colorize_sha Commit.truncate_sha(hook_attrs['id'])
author = hook_attrs['author']['name']
files = colorize_nb_files(files_count commit)
title = commit.title
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 2fff6b0105d..2cd87895c55 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,8 +3,8 @@ class PostReceive
include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
- if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
- repo_path.gsub!(path[1].to_s, "")
+ if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) }
+ repo_path.gsub!(repository_storage[1]['path'].to_s, "")
else
log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
deleted file mode 100644
index b70df5a1afa..00000000000
--- a/app/workers/stuck_ci_builds_worker.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class StuckCiBuildsWorker
- include Sidekiq::Worker
- include CronjobQueue
-
- BUILD_STUCK_TIMEOUT = 1.day
-
- def perform
- Rails.logger.info 'Cleaning stuck builds'
-
- builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
- builds.find_each(batch_size: 50).each do |build|
- Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
- build.drop
- end
-
- # Update builds that failed to drop
- builds.update_all(status: 'failed')
- end
-end
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
new file mode 100644
index 00000000000..ae8c980c9e4
--- /dev/null
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -0,0 +1,59 @@
+class StuckCiJobsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'.freeze
+
+ BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
+ BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
+ BUILD_PENDING_STUCK_TIMEOUT = 1.hour
+
+ def perform
+ return unless try_obtain_lease
+
+ Rails.logger.info "#{self.class}: Cleaning stuck builds"
+
+ drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT
+ drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT
+ drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT
+
+ remove_lease
+ end
+
+ private
+
+ def try_obtain_lease
+ @uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
+ end
+
+ def remove_lease
+ Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
+ end
+
+ def drop(status, timeout)
+ search(status, timeout) do |build|
+ drop_build :outdated, build, status, timeout
+ end
+ end
+
+ def drop_stuck(status, timeout)
+ search(status, timeout) do |build|
+ return unless build.stuck?
+ drop_build :stuck, build, status, timeout
+ end
+ end
+
+ def search(status, timeout)
+ builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
+ builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
+ yield(build)
+ end
+ end
+
+ def drop_build(type, build, status, timeout)
+ Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
+ Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
+ b.drop
+ end
+ end
+end
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
new file mode 100644
index 00000000000..e43bbe35de9
--- /dev/null
+++ b/app/workers/system_hook_push_worker.rb
@@ -0,0 +1,8 @@
+class SystemHookPushWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(push_data, hook_id)
+ SystemHooksService.new.execute_hooks(push_data, hook_id)
+ end
+end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index acc4d858136..89ae17cef37 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -10,8 +10,5 @@ class UpdateMergeRequestsWorker
return unless user
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
-
- push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, [])
- SystemHooksService.new.execute_hooks(push_data, :push_hooks)
end
end
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
new file mode 100644
index 00000000000..78931f1258f
--- /dev/null
+++ b/app/workers/upload_checksum_worker.rb
@@ -0,0 +1,12 @@
+class UploadChecksumWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(upload_id)
+ upload = Upload.find(upload_id)
+ upload.calculate_checksum
+ upload.save!
+ rescue ActiveRecord::RecordNotFound
+ Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping")
+ end
+end
diff --git a/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
new file mode 100644
index 00000000000..4a1a199673c
--- /dev/null
+++ b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
@@ -0,0 +1,4 @@
+---
+title: Deleting a user doesn't delete issues they've created/are assigned to
+merge_request: 7393
+author:
diff --git a/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
new file mode 100644
index 00000000000..1b7e294bd67
--- /dev/null
+++ b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
@@ -0,0 +1,4 @@
+---
+title: "GET 'projects/:id/repository/commits' endpoint improvements"
+merge_request: 9679
+author: George Andrinopoulos, Jordan Ryan Reuter
diff --git a/changelogs/unreleased/14748-runner-version-in-admin-views.yml b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
new file mode 100644
index 00000000000..2478a81c824
--- /dev/null
+++ b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
@@ -0,0 +1,4 @@
+---
+title: Add runner version to /admin/runners view
+merge_request: 8733
+author: Jonathon Reinhart
diff --git a/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
new file mode 100644
index 00000000000..f247fe35439
--- /dev/null
+++ b/changelogs/unreleased/1648-remove-remnants-of-git-annex-from-ce.yml
@@ -0,0 +1,4 @@
+---
+title: Remove remnants of git annex support.
+merge_request:
+author:
diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml
deleted file mode 100644
index 12f2998d1c8..00000000000
--- a/changelogs/unreleased/17662-rename-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere
-merge_request: 8787
-author:
diff --git a/changelogs/unreleased/18962-update-issues-button-jumps.yml b/changelogs/unreleased/18962-update-issues-button-jumps.yml
new file mode 100644
index 00000000000..7be136ac4ff
--- /dev/null
+++ b/changelogs/unreleased/18962-update-issues-button-jumps.yml
@@ -0,0 +1,4 @@
+---
+title: Align bulk update issues button to the right
+merge_request:
+author:
diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml
new file mode 100644
index 00000000000..fa89d94e0f3
--- /dev/null
+++ b/changelogs/unreleased/1937-https-clone-url-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add the Username to the HTTP(S) clone URL of a Repository
+merge_request: 9347
+author: Jan Christophersen
diff --git a/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml
new file mode 100644
index 00000000000..eceb2b9fac6
--- /dev/null
+++ b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml
@@ -0,0 +1,4 @@
+---
+title: Hide issue info when project issues are disabled
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
new file mode 100644
index 00000000000..199f1edec8b
--- /dev/null
+++ b/changelogs/unreleased/19742-permalink-blame-button-line-number-hash-links.yml
@@ -0,0 +1,4 @@
+---
+title: Update permalink/blame buttons with line number fragment hash
+merge_request:
+author:
diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
deleted file mode 100644
index 965d0648adf..00000000000
--- a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb
-merge_request:
-author:
diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
deleted file mode 100644
index eda872049fd..00000000000
--- a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added labels empty state
-merge_request: 7443
-author:
diff --git a/changelogs/unreleased/21451-allow-disable-mr-link.yml b/changelogs/unreleased/21451-allow-disable-mr-link.yml
new file mode 100644
index 00000000000..ef99970a7a2
--- /dev/null
+++ b/changelogs/unreleased/21451-allow-disable-mr-link.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to disable Merge Request URL on push
+merge_request: 9663
+author: Alex Sanford
diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
deleted file mode 100644
index bd6c9d7521e..00000000000
--- a/changelogs/unreleased/21518_recaptcha_spam_issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use reCaptcha when an issue is identified as a spam
-merge_request: 8846
-author:
diff --git a/changelogs/unreleased/21605-allow-html5-details.yml b/changelogs/unreleased/21605-allow-html5-details.yml
new file mode 100644
index 00000000000..b0c654783d9
--- /dev/null
+++ b/changelogs/unreleased/21605-allow-html5-details.yml
@@ -0,0 +1,4 @@
+---
+title: SanitizationFilter allows html5 details and summary tags
+merge_request: 6568
+author:
diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml
deleted file mode 100644
index f43c1925ad0..00000000000
--- a/changelogs/unreleased/22007-unify-projects-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unify projects search by removing /projects/:search endpoint
-merge_request: 8877
-author:
diff --git a/changelogs/unreleased/22466-task-list-alignment.yml b/changelogs/unreleased/22466-task-list-alignment.yml
new file mode 100644
index 00000000000..6e6ccb873ec
--- /dev/null
+++ b/changelogs/unreleased/22466-task-list-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Align task list checkboxes
+merge_request: 6487
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/22562-todos-filters.yml b/changelogs/unreleased/22562-todos-filters.yml
new file mode 100644
index 00000000000..9cca138744a
--- /dev/null
+++ b/changelogs/unreleased/22562-todos-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort dropdown reflow issue
+merge_request: 9533
+author: Jarkko Tuunanen
diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
deleted file mode 100644
index 2c6883bcf7b..00000000000
--- a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow creating protected branches when user can merge to such branch
-merge_request: 8458
-author:
diff --git a/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml
new file mode 100644
index 00000000000..a53e7d77c16
--- /dev/null
+++ b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml
@@ -0,0 +1,4 @@
+---
+title: Add spec for todo with target_type Commit
+merge_request: 9351
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
deleted file mode 100644
index 57106e8c676..00000000000
--- a/changelogs/unreleased/22974-trigger-service-events-through-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds service trigger events to api
-merge_request: 8324
-author:
diff --git a/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
new file mode 100644
index 00000000000..f7c856040e0
--- /dev/null
+++ b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
@@ -0,0 +1,4 @@
+---
+title: Make Git history follow renames again by performing the --skip in Ruby
+merge_request:
+author:
diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
deleted file mode 100644
index 268be6b9b83..00000000000
--- a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs
-merge_request: 8056
-author: twonegatives
diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml
deleted file mode 100644
index dde8b2d1815..00000000000
--- a/changelogs/unreleased/23634-remove-project-grouping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't group issues by project on group-level and dashboard issue indexes.
-merge_request: 8111
-author: Bernardo Castro
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
deleted file mode 100644
index 587ef4f9a73..00000000000
--- a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix disable storing of sensitive information when importing a new repo
-merge_request: 8885
-author: Bernard Pietraga
diff --git a/changelogs/unreleased/23948-assign-to-me.yml b/changelogs/unreleased/23948-assign-to-me.yml
new file mode 100644
index 00000000000..d73aa92b0e9
--- /dev/null
+++ b/changelogs/unreleased/23948-assign-to-me.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add Assign to me link to Merge Request and Issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/23993-drop-ci_projects.yml b/changelogs/unreleased/23993-drop-ci_projects.yml
new file mode 100644
index 00000000000..ee9cf774e37
--- /dev/null
+++ b/changelogs/unreleased/23993-drop-ci_projects.yml
@@ -0,0 +1,6 @@
+---
+title: Drop unused ci_projects table and some unused project_id columns,
+ then rename gl_project_id to project_id. Stop exporting job trace when
+ exporting projects.
+merge_request: 9378
+author: David Wagner
diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml
new file mode 100644
index 00000000000..bcc6c6957a1
--- /dev/null
+++ b/changelogs/unreleased/24137-issuable-permalink.yml
@@ -0,0 +1,4 @@
+---
+title: Link issuable reference to itself in meta-header
+merge_request: 9641
+author: mhasbini
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
deleted file mode 100644
index 14e80cacbfb..00000000000
--- a/changelogs/unreleased/24147-delete-env-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds back ability to stop all environments
-merge_request: 7379
-author:
diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml
new file mode 100644
index 00000000000..c57ffed6b45
--- /dev/null
+++ b/changelogs/unreleased/24166-close-builds-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent builds dropdown to close when the user clicks in a build
+merge_request:
+author:
diff --git a/changelogs/unreleased/24421-personal-milestone-count-badges.yml b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
new file mode 100644
index 00000000000..8bbc1ed2dde
--- /dev/null
+++ b/changelogs/unreleased/24421-personal-milestone-count-badges.yml
@@ -0,0 +1,4 @@
+---
+title: Add dashboard and group milestones count badges
+merge_request: 9836
+author: Alex Braha Stoll
diff --git a/changelogs/unreleased/24501-new-file-existing-branch.yml b/changelogs/unreleased/24501-new-file-existing-branch.yml
new file mode 100644
index 00000000000..31c66b2a978
--- /dev/null
+++ b/changelogs/unreleased/24501-new-file-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New file from interface on existing branch
+merge_request: 8427
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
deleted file mode 100644
index fd671d04a9f..00000000000
--- a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Force new password after password reset via API
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/24683-sidebar-spinners.yml b/changelogs/unreleased/24683-sidebar-spinners.yml
new file mode 100644
index 00000000000..3fec273152f
--- /dev/null
+++ b/changelogs/unreleased/24683-sidebar-spinners.yml
@@ -0,0 +1,4 @@
+---
+title: hide loading spinners for server-rendered sidebar fields
+merge_request:
+author:
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
deleted file mode 100644
index 13de5db5e41..00000000000
--- a/changelogs/unreleased/24716-fix-ctrl-click-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Ctrl+Click support for Todos and Merge Request page tabs
-merge_request: 8898
-author:
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
deleted file mode 100644
index b735fb57649..00000000000
--- a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor MergeRequests::BuildService
-merge_request: 8462
-author: Rydkin Maxim
diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
deleted file mode 100644
index be66c370f36..00000000000
--- a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Allows to search within project by commit hash'
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml
deleted file mode 100644
index de35cad3dd6..00000000000
--- a/changelogs/unreleased/24923_nested_tasks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix nested tasks in ordered list
-merge_request: 8626
-author:
diff --git a/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml
new file mode 100644
index 00000000000..3b90466e3af
--- /dev/null
+++ b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml
@@ -0,0 +1,4 @@
+---
+title: Fix typo in Gitlab config file
+merge_request: 9702
+author: medied
diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
deleted file mode 100644
index d35ad0be0db..00000000000
--- a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username
-merge_request:
-author:
diff --git a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
deleted file mode 100644
index 56e03a48692..00000000000
--- a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent removal of input fields if it is the parent dropdown element
-merge_request: 8397
-author:
diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
deleted file mode 100644
index 50a5c879446..00000000000
--- a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove flash warning from login page
-merge_request: 8864
-author: Gerald J. Padilla
diff --git a/changelogs/unreleased/25367-add-impersonation-token.yml b/changelogs/unreleased/25367-add-impersonation-token.yml
new file mode 100644
index 00000000000..4a30f960036
--- /dev/null
+++ b/changelogs/unreleased/25367-add-impersonation-token.yml
@@ -0,0 +1,4 @@
+---
+title: Manage user personal access tokens through api and add impersonation tokens
+merge_request: 9099
+author: Simon Vocella
diff --git a/changelogs/unreleased/25437-just-emoji.yml b/changelogs/unreleased/25437-just-emoji.yml
new file mode 100644
index 00000000000..ceb81a47f2d
--- /dev/null
+++ b/changelogs/unreleased/25437-just-emoji.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce /award slash command; Allow posting of just an emoji in comment
+merge_request: 9382
+author: mhasbini
diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml
deleted file mode 100644
index dac90eaa34d..00000000000
--- a/changelogs/unreleased/25460-replace-word-users-with-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace word user with member
-merge_request: 8872
-author:
diff --git a/changelogs/unreleased/25503_issues_finder_performance.yml b/changelogs/unreleased/25503_issues_finder_performance.yml
new file mode 100644
index 00000000000..87964269c6d
--- /dev/null
+++ b/changelogs/unreleased/25503_issues_finder_performance.yml
@@ -0,0 +1,4 @@
+---
+title: Filter by projects in the end of search
+merge_request: 9030
+author:
diff --git a/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
new file mode 100644
index 00000000000..5b755a8bc32
--- /dev/null
+++ b/changelogs/unreleased/25515-delegate-single-discussion-to-new-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Create a new issue for a single discussion in a Merge Request
+merge_request: 8266
+author: Bob Van Landuyt
diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
deleted file mode 100644
index d7f950d7be9..00000000000
--- a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove turbolinks.
-merge_request: !8570
-author:
diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
deleted file mode 100644
index f74e9fa8b6d..00000000000
--- a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update pipeline and commit links when CI status is updated
-merge_request: 8351
-author:
diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
deleted file mode 100644
index 9506692dd40..00000000000
--- a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert pipeline action icons to svg to have them propperly positioned
-merge_request:
-author:
diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
new file mode 100644
index 00000000000..580d1074aa7
--- /dev/null
+++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to create issue for failing build
+merge_request: 9391
+author: Alex Sanford
diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
deleted file mode 100644
index e67a9c0da15..00000000000
--- a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove rogue scrollbars for issue comments with inline elements
-merge_request:
-author:
diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml
deleted file mode 100644
index fc3f1af5b61..00000000000
--- a/changelogs/unreleased/26059-segoe-ui-vertical.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Align Segoe UI label text
-merge_request:
-author:
diff --git a/changelogs/unreleased/26068_tasklist_issue.yml b/changelogs/unreleased/26068_tasklist_issue.yml
deleted file mode 100644
index c938351b8a7..00000000000
--- a/changelogs/unreleased/26068_tasklist_issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don’t count tasks that are not defined as list items correctly
-merge_request: 8526
-author:
diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
deleted file mode 100644
index b2f5294d380..00000000000
--- a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add sorting pipeline for a commit
-merge_request: 8319
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/26136-list-repository-tree-api-doc.yml b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml
new file mode 100644
index 00000000000..85d8bc6ca8a
--- /dev/null
+++ b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Make documentation of list repository tree API call more detailed
+merge_request: 9532
+author: Marius Kleiner
diff --git a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml b/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
deleted file mode 100644
index 565672917b2..00000000000
--- a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Color + and - signs in diffs to increase code legibility
-merge_request:
-author:
diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
new file mode 100644
index 00000000000..fb00d46ea1f
--- /dev/null
+++ b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
@@ -0,0 +1,4 @@
+---
+title: Don't show links to tag a commit for users that are not permitted
+merge_request: 8407
+author:
diff --git a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
new file mode 100644
index 00000000000..827224abf5a
--- /dev/null
+++ b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
@@ -0,0 +1,4 @@
+---
+title: Changed dropdown style slightly
+merge_request:
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
new file mode 100644
index 00000000000..c44f3d4d3dc
--- /dev/null
+++ b/changelogs/unreleased/26236-monospace-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Change gfm textarea to use monospace font
+merge_request:
+author:
diff --git a/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml
new file mode 100644
index 00000000000..6ee8e5724bc
--- /dev/null
+++ b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml
@@ -0,0 +1,4 @@
+---
+title: Show public RSS feeds to anonymous users
+merge_request: 9596
+author: Michael Kozono
diff --git a/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
new file mode 100644
index 00000000000..ce888baa32f
--- /dev/null
+++ b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Clean-up Groups navigation order
+merge_request: 9309
+author:
diff --git a/changelogs/unreleased/26348-cleanup-navigation-order.yml b/changelogs/unreleased/26348-cleanup-navigation-order.yml
new file mode 100644
index 00000000000..d5324f9e025
--- /dev/null
+++ b/changelogs/unreleased/26348-cleanup-navigation-order.yml
@@ -0,0 +1,4 @@
+---
+title: Clean-up Project navigation order
+merge_request: 9272
+author:
diff --git a/changelogs/unreleased/26371-native-emojis-v3-code.yml b/changelogs/unreleased/26371-native-emojis-v3-code.yml
new file mode 100644
index 00000000000..88346711490
--- /dev/null
+++ b/changelogs/unreleased/26371-native-emojis-v3-code.yml
@@ -0,0 +1,4 @@
+---
+title: Use native unicode emojis
+merge_request:
+author:
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
deleted file mode 100644
index fb5274e5253..00000000000
--- a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve button accessibility on pipelines page
-merge_request: 8561
-author:
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
deleted file mode 100644
index 351c53bd076..00000000000
--- a/changelogs/unreleased/26447-fix-tab-list-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix tab index order on branch commits list page
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
deleted file mode 100644
index 87ae8233c4a..00000000000
--- a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Sort by Recent Sign-in in Admin Area
-merge_request: 8637
-author: Poornima M
diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
new file mode 100644
index 00000000000..e82cbf00cfb
--- /dev/null
+++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Strip reference prefixes on branch creation
+merge_request: 8498
+author: Matthieu Tardy
diff --git a/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
new file mode 100644
index 00000000000..6fc4615dab8
--- /dev/null
+++ b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Combined deploy keys, push rules, protect branches and mirror repository settings options into a single one called
+ Repository
+merge_request:
+author:
diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
deleted file mode 100644
index 31f1812c6f8..00000000000
--- a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add hover style to copy icon on commit page header
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26790-label-color-todos.yml b/changelogs/unreleased/26790-label-color-todos.yml
new file mode 100644
index 00000000000..74084473d81
--- /dev/null
+++ b/changelogs/unreleased/26790-label-color-todos.yml
@@ -0,0 +1,4 @@
+---
+title: fix background color for labels mention in todo
+merge_request: 9155
+author: mhasbini
diff --git a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml
deleted file mode 100644
index 182a9ae126b..00000000000
--- a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: prevent diff unfolding link from appearing when there are no more lines to
- show
-merge_request: 8761
-author:
diff --git a/changelogs/unreleased/26847-api-pipelines-use-basic.yml b/changelogs/unreleased/26847-api-pipelines-use-basic.yml
new file mode 100644
index 00000000000..2034a4ba080
--- /dev/null
+++ b/changelogs/unreleased/26847-api-pipelines-use-basic.yml
@@ -0,0 +1,4 @@
+---
+title: Expose pipelines as PipelineBasic `api/v3/projects/:id/pipelines`
+merge_request: 8875
+author:
diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
deleted file mode 100644
index fb65b068b23..00000000000
--- a/changelogs/unreleased/26852-fix-slug-for-openshift.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG
-merge_request: 8638
-author:
diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
deleted file mode 100644
index 8dfabf87c2a..00000000000
--- a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove hover animation from row elements
-merge_request:
-author:
diff --git a/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
new file mode 100644
index 00000000000..3d6400cba76
--- /dev/null
+++ b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
@@ -0,0 +1,4 @@
+---
+title: Add all available statuses to scope filter for project builds endpoint
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml b/changelogs/unreleased/26881-backup-fails-if-data-changes.yml
deleted file mode 100644
index 00bf105560b..00000000000
--- a/changelogs/unreleased/26881-backup-fails-if-data-changes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `copy` backup strategy to combat file changed errors
-merge_request: 8728
-author:
diff --git a/changelogs/unreleased/26900-pipelines-tabs.yml b/changelogs/unreleased/26900-pipelines-tabs.yml
new file mode 100644
index 00000000000..f08514c621f
--- /dev/null
+++ b/changelogs/unreleased/26900-pipelines-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Adds Pending and Finished tabs to pipelines page
+merge_request:
+author:
diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
deleted file mode 100644
index ea567437ac2..00000000000
--- a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes hover cursor on pipeline pagenation
-merge_request: 9003
-author:
diff --git a/changelogs/unreleased/26947-build-status-self-link.yml b/changelogs/unreleased/26947-build-status-self-link.yml
deleted file mode 100644
index 15c5821874e..00000000000
--- a/changelogs/unreleased/26947-build-status-self-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add link verification to badge partial in order to render a badge without a link
-merge_request: 8740
-author:
diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
deleted file mode 100644
index c5c57af5aaf..00000000000
--- a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve pipeline status icon linking in widgets
-merge_request:
-author:
diff --git a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml b/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
deleted file mode 100644
index 7cb5e4b273d..00000000000
--- a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix commit title bar and repository view copy clipboard button order on last commit in repository view
-merge_request:
-author:
diff --git a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml b/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
deleted file mode 100644
index f0301c849b6..00000000000
--- a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix mini-pipeline stage tooltip text wrapping
-merge_request:
-author:
diff --git a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml b/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
deleted file mode 100644
index b5584749098..00000000000
--- a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent copying of line numbers in parallel diff view
-merge_request: 8706
-author:
diff --git a/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
new file mode 100644
index 00000000000..a9f70e339c0
--- /dev/null
+++ b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
@@ -0,0 +1,4 @@
+---
+title: Add housekeeping endpoint for Projects API
+merge_request: 9421
+author:
diff --git a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
new file mode 100644
index 00000000000..44aae486574
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo mark all as done to Todos
+merge_request: 9890
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
new file mode 100644
index 00000000000..2e6c10a6bfe
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo to Todos in the Done tab
+merge_request: 8782
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
new file mode 100644
index 00000000000..ee236310a71
--- /dev/null
+++ b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
@@ -0,0 +1,4 @@
+---
+title: API: Add environment stop action
+merge_request: 8808
+author:
diff --git a/changelogs/unreleased/27174-filter-filters.yml b/changelogs/unreleased/27174-filter-filters.yml
new file mode 100644
index 00000000000..0da1e4d5d3b
--- /dev/null
+++ b/changelogs/unreleased/27174-filter-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent filtering issues by multiple Milestones or Authors
+merge_request:
+author:
diff --git a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml b/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
deleted file mode 100644
index 52406bba464..00000000000
--- a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Updated builds info link on the project settings page
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
deleted file mode 100644
index 3f902fb324e..00000000000
--- a/changelogs/unreleased/27240-make-progress-bars-consistent.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 27240 Make progress bars consistent
-merge_request:
-author:
diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
new file mode 100644
index 00000000000..4ea52a70e89
--- /dev/null
+++ b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
@@ -0,0 +1,4 @@
+---
+title: Include time tracking attributes in webhooks payload
+merge_request: 9942
+author:
diff --git a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml b/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
deleted file mode 100644
index 9456251025b..00000000000
--- a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fixed small mini pipeline graph line glitch
-merge_request: 8804
-author:
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
deleted file mode 100644
index 293aab67d39..00000000000
--- a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unify MR diff file button style
-merge_request: 8874
-author:
diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
deleted file mode 100644
index 502927cd160..00000000000
--- a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only render hr when user can't archive project.
-merge_request: !8917
-author:
diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
deleted file mode 100644
index 79316abbaf7..00000000000
--- a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline graph vertical spacing in Firefox and Safari
-merge_request: 8886
-author:
diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml
deleted file mode 100644
index 191b530aee8..00000000000
--- a/changelogs/unreleased/27352-search-label-filter-header.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 27352-search-label-filter-header
-merge_request:
-author:
diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml
new file mode 100644
index 00000000000..62cac9bbbd3
--- /dev/null
+++ b/changelogs/unreleased/27354-navigation-new-button.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add the New Project button in nav bar
+merge_request:
+author:
diff --git a/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
new file mode 100644
index 00000000000..a116c68ad87
--- /dev/null
+++ b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
@@ -0,0 +1,4 @@
+---
+title: Speed up project dashboard by caching pipeline status and eager loading routes
+merge_request: 9903
+author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
deleted file mode 100644
index f3ce1709518..00000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include :author, :project, and :target in Event.with_associations
-merge_request:
-author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
deleted file mode 100644
index 3f6d922f2a0..00000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't instantiate AR objects in Event.in_projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
deleted file mode 100644
index dc400d65006..00000000000
--- a/changelogs/unreleased/27484-environment-show-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't capitalize environment name in show page
-merge_request:
-author:
diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml
deleted file mode 100644
index 5135ff0fd60..00000000000
--- a/changelogs/unreleased/27488-fix-jwt-version.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update and pin the `jwt` gem to ~> 1.5.6
-merge_request:
-author:
diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml
deleted file mode 100644
index 798c01f3238..00000000000
--- a/changelogs/unreleased/27494-environment-list-column-headers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
-merge_request:
-author:
diff --git a/changelogs/unreleased/27501-api-use-visibility-everywhere.yml b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml
new file mode 100644
index 00000000000..f1b70687878
--- /dev/null
+++ b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml
@@ -0,0 +1,4 @@
+---
+title: "API: Use `visibility` as string parameter everywhere"
+merge_request: 9337
+author:
diff --git a/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml
new file mode 100644
index 00000000000..3050b072863
--- /dev/null
+++ b/changelogs/unreleased/27520-option-to-prevent-signing-in-from-multiple-ips.yml
@@ -0,0 +1,4 @@
+---
+title: Option to prevent signing in from multiple ips
+merge_request: 8998
+author:
diff --git a/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml b/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml
new file mode 100644
index 00000000000..a4ef2b23aaa
--- /dev/null
+++ b/changelogs/unreleased/27523-make-stuck-build-detection-more-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Make stuck builds detection more performant
+merge_request: 9025
+author:
diff --git a/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
new file mode 100644
index 00000000000..4436b4bee68
--- /dev/null
+++ b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes job dropdown action throws error in js console
+merge_request: 9182
+author:
diff --git a/changelogs/unreleased/27532_api_changes.yml b/changelogs/unreleased/27532_api_changes.yml
new file mode 100644
index 00000000000..778469d5a86
--- /dev/null
+++ b/changelogs/unreleased/27532_api_changes.yml
@@ -0,0 +1,4 @@
+---
+title: Use iids as filter parameter
+merge_request: 9096
+author:
diff --git a/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
new file mode 100644
index 00000000000..5c738af7704
--- /dev/null
+++ b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_assignee_spec
+merge_request: 9711
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
deleted file mode 100644
index a5bb37ec8a9..00000000000
--- a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes flickering of avatar border in mention dropdown
-merge_request: 8950
-author:
diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml
deleted file mode 100644
index 958621a43a1..00000000000
--- a/changelogs/unreleased/27632_fix_mr_widget_url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix MR widget url
-merge_request: 8989
-author:
diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
deleted file mode 100644
index 0531ef2c038..00000000000
--- a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Layer award emoji dropdown over the right sidebar
-merge_request: 9004
-author:
diff --git a/changelogs/unreleased/27656-doc-ci-enable-ci.yml b/changelogs/unreleased/27656-doc-ci-enable-ci.yml
deleted file mode 100644
index e6315d683d4..00000000000
--- a/changelogs/unreleased/27656-doc-ci-enable-ci.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update doc for enabling or disabling GitLab CI
-merge_request: 8965
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/27762-add-default-artifacts-expiration.yml b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
new file mode 100644
index 00000000000..27fa77ed04d
--- /dev/null
+++ b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
@@ -0,0 +1,4 @@
+---
+title: Add admin setting for default artifacts expiration
+merge_request: 9219
+author:
diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
deleted file mode 100644
index aa89d9f9850..00000000000
--- a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Give ci status text on pipeline graph a better font-weight
-merge_request:
-author:
diff --git a/changelogs/unreleased/27778-a11y-sidebar.yml b/changelogs/unreleased/27778-a11y-sidebar.yml
new file mode 100644
index 00000000000..fb37d7fdb35
--- /dev/null
+++ b/changelogs/unreleased/27778-a11y-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Improves a11y in sidebar by adding aria-hidden attributes in i tags and by
+ fixing two broken aria-hidden attributes
+merge_request:
+author:
diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
deleted file mode 100644
index ee2431869f0..00000000000
--- a/changelogs/unreleased/27822-default-bulk-assign-labels.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add default labels to bulk assign dropdowns
-merge_request:
-author:
diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml
new file mode 100644
index 00000000000..87b1f0c5572
--- /dev/null
+++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml
@@ -0,0 +1,4 @@
+---
+title: Enhanced filter issues layout for better mobile experiance
+merge_request: 9280
+author: Pratik Borsadiya
diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
deleted file mode 100644
index 89e2bdc69bc..00000000000
--- a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only return target project's comments for a commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
deleted file mode 100644
index 4251754618b..00000000000
--- a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes Pipelines table is not showing branch name for commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml
deleted file mode 100644
index 52b7e96682d..00000000000
--- a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Trigger autocomplete after selecting a slash command
-merge_request: 9117
-author:
diff --git a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml b/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
deleted file mode 100644
index 79a54429ee8..00000000000
--- a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix regression where cmd-click stopped working for todos and merge request
- tabs
-merge_request:
-author:
diff --git a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml b/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
deleted file mode 100644
index f7bdb62b7f3..00000000000
--- a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix stray pipelines API request when showing MR
-merge_request:
-author:
diff --git a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
deleted file mode 100644
index b7505e28401..00000000000
--- a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Merge request pipelines displays JSON
-merge_request:
-author:
diff --git a/changelogs/unreleased/27934-left-align-logo.yml b/changelogs/unreleased/27934-left-align-logo.yml
new file mode 100644
index 00000000000..d4e5e169465
--- /dev/null
+++ b/changelogs/unreleased/27934-left-align-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Left align logo
+merge_request:
+author:
diff --git a/changelogs/unreleased/27934-left-align-nav.yml b/changelogs/unreleased/27934-left-align-nav.yml
deleted file mode 100644
index 6c45e4ce175..00000000000
--- a/changelogs/unreleased/27934-left-align-nav.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Left align navigation
-merge_request:
-author:
diff --git a/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
new file mode 100644
index 00000000000..adc129d8dca
--- /dev/null
+++ b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
@@ -0,0 +1,4 @@
+---
+title: Uploaded files which content can change now require revalidation on each page load
+merge_request: 9453
+author:
diff --git a/changelogs/unreleased/27939-fix-current-build-arrow.yml b/changelogs/unreleased/27939-fix-current-build-arrow.yml
deleted file mode 100644
index 280ab090f2c..00000000000
--- a/changelogs/unreleased/27939-fix-current-build-arrow.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix current build arrow indicator
-merge_request:
-author:
diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
deleted file mode 100644
index fcbd48b0357..00000000000
--- a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix contribution activity alignment
-merge_request:
-author:
diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
deleted file mode 100644
index 1dfabd3813b..00000000000
--- a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add space between text and loading icon in Megre Request Widget
-merge_request: 9119
-author:
diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
deleted file mode 100644
index d9f78db4bec..00000000000
--- a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show Pipeline(not Job) in MR desktop notification
-merge_request:
-author:
diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml
deleted file mode 100644
index ba418d86433..00000000000
--- a/changelogs/unreleased/27963-tooltips-jobs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix tooltips in mini pipeline graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml b/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
deleted file mode 100644
index 6fa13395a7d..00000000000
--- a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display loading indicator when filtering ref switcher dropdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/27978-improve-task-list-ux.yml b/changelogs/unreleased/27978-improve-task-list-ux.yml
new file mode 100644
index 00000000000..a6bd99da82e
--- /dev/null
+++ b/changelogs/unreleased/27978-improve-task-list-ux.yml
@@ -0,0 +1,4 @@
+---
+title: Only add a newline in the Markdown Editor if the current line is not empty
+merge_request: 9455
+author: Jan Christophersen
diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
deleted file mode 100644
index e4287d6276c..00000000000
--- a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show pipeline graph in MR widget if there are any stages
-merge_request:
-author:
diff --git a/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
new file mode 100644
index 00000000000..c6ba9572f26
--- /dev/null
+++ b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add `requirements: { id: /.+/ }` for all projects and groups namespaced API
+ routes'
+merge_request: 9944
+author:
diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml
deleted file mode 100644
index 703d34a5ede..00000000000
--- a/changelogs/unreleased/27991-success-with-warnings-caret.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix icon colors in merge request widget mini graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml
new file mode 100644
index 00000000000..06bb669ceac
--- /dev/null
+++ b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml
@@ -0,0 +1,4 @@
+---
+title: Default to subtle MR mege button until CI status is available
+merge_request:
+author:
diff --git a/changelogs/unreleased/28019-make-builds-show-faster.yml b/changelogs/unreleased/28019-make-builds-show-faster.yml
new file mode 100644
index 00000000000..bbfea0e4c88
--- /dev/null
+++ b/changelogs/unreleased/28019-make-builds-show-faster.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid calling Build#trace_with_state for performance
+merge_request: 9149
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
deleted file mode 100644
index be2a0afbc52..00000000000
--- a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve blockquote formatting in notification emails
-merge_request:
-author:
diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml
new file mode 100644
index 00000000000..6f4082d7684
--- /dev/null
+++ b/changelogs/unreleased/28030-infinite-offset.yml
@@ -0,0 +1,4 @@
+---
+title: allow offset query parameter for infinite list pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/28032-tooltips-file-name.yml b/changelogs/unreleased/28032-tooltips-file-name.yml
deleted file mode 100644
index 9fe11e7c2b6..00000000000
--- a/changelogs/unreleased/28032-tooltips-file-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds container to tooltip in order to make it work with overflow:hidden in
- parent element
-merge_request:
-author:
diff --git a/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
new file mode 100644
index 00000000000..e0e826a67f8
--- /dev/null
+++ b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
@@ -0,0 +1,4 @@
+---
+title: Only show public emails in atom feeds
+merge_request:
+author:
diff --git a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml
deleted file mode 100644
index 1b2e678bbed..00000000000
--- a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Restore pagination to admin abuse reports
-merge_request:
-author:
diff --git a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml b/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml
deleted file mode 100644
index d70b5ef8fd5..00000000000
--- a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Spam check and reCAPTCHA improvements
-merge_request:
-author:
diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
new file mode 100644
index 00000000000..feca38ff083
--- /dev/null
+++ b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Use toggle button to expand / collapse mulit-nested groups
+merge_request: 9501
+author:
diff --git a/changelogs/unreleased/28257-issues-iids.yml b/changelogs/unreleased/28257-issues-iids.yml
new file mode 100644
index 00000000000..0a85504a8de
--- /dev/null
+++ b/changelogs/unreleased/28257-issues-iids.yml
@@ -0,0 +1,4 @@
+---
+title: API issues - support filtering by iids
+merge_request:
+author:
diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
new file mode 100644
index 00000000000..6e3cd8a60d8
--- /dev/null
+++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Document U2F limitations with multiple URLs
+merge_request: 9300
+author:
diff --git a/changelogs/unreleased/28357-colon-search.yml b/changelogs/unreleased/28357-colon-search.yml
deleted file mode 100644
index 4bbb0dc12b2..00000000000
--- a/changelogs/unreleased/28357-colon-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow searching issues for strings containing colons
-merge_request:
-author:
diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
new file mode 100644
index 00000000000..faf1e89ed94
--- /dev/null
+++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
@@ -0,0 +1,4 @@
+---
+title: Remove markup that was showing in tooltip for renamed files
+merge_request: 9374
+author:
diff --git a/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
new file mode 100644
index 00000000000..6fc89fd91dd
--- /dev/null
+++ b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes includes line number during unfold copy n paste in parallel diff view
+merge_request: 9365
+author:
diff --git a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
new file mode 100644
index 00000000000..dd94b3fe663
--- /dev/null
+++ b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong message on starred projects filtering
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28410-dropdown-styling.yml b/changelogs/unreleased/28410-dropdown-styling.yml
new file mode 100644
index 00000000000..2a7af1dd6e8
--- /dev/null
+++ b/changelogs/unreleased/28410-dropdown-styling.yml
@@ -0,0 +1,4 @@
+---
+title: Add badges to global dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/28447-hybrid-repository-storages.yml b/changelogs/unreleased/28447-hybrid-repository-storages.yml
new file mode 100644
index 00000000000..00dfc5781b9
--- /dev/null
+++ b/changelogs/unreleased/28447-hybrid-repository-storages.yml
@@ -0,0 +1,4 @@
+---
+title: Update storage settings to allow extra values per repository storage
+merge_request: 9597
+author:
diff --git a/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
new file mode 100644
index 00000000000..196a9b788ea
--- /dev/null
+++ b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
@@ -0,0 +1,4 @@
+---
+title: test compiling production assets and generate webpack bundle report in CI
+merge_request: 9396
+author:
diff --git a/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
new file mode 100644
index 00000000000..dbbe8a19204
--- /dev/null
+++ b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
@@ -0,0 +1,4 @@
+---
+title: Present GitLab version for each V3 to V4 API change on v3_to_v4.md
+merge_request:
+author:
diff --git a/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
new file mode 100644
index 00000000000..67dbc30e760
--- /dev/null
+++ b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
@@ -0,0 +1,4 @@
+---
+title: Adds pipeline mini-graph to system information box in Commit View
+merge_request:
+author:
diff --git a/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
new file mode 100644
index 00000000000..660a881e094
--- /dev/null
+++ b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes large file name tooltip cutoff in diff header
+merge_request: 9529
+author:
diff --git a/changelogs/unreleased/28516-default-kubernetes-namespace.yml b/changelogs/unreleased/28516-default-kubernetes-namespace.yml
new file mode 100644
index 00000000000..9fa5c681a53
--- /dev/null
+++ b/changelogs/unreleased/28516-default-kubernetes-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Make a default namespace of Kubernetes service to contain project ID
+merge_request:
+author:
diff --git a/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
new file mode 100644
index 00000000000..eda5764c13e
--- /dev/null
+++ b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
@@ -0,0 +1,4 @@
+---
+title: Document when current coverage configuration option was introduced
+merge_request: 9443
+author:
diff --git a/changelogs/unreleased/28538-restore-nav-shortcuts.yml b/changelogs/unreleased/28538-restore-nav-shortcuts.yml
new file mode 100644
index 00000000000..07b39cd50d1
--- /dev/null
+++ b/changelogs/unreleased/28538-restore-nav-shortcuts.yml
@@ -0,0 +1,4 @@
+---
+title: Restore keyboard shortcuts for "Activity" and "Charts"
+merge_request: 9680
+author:
diff --git a/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml
new file mode 100644
index 00000000000..ada726c9048
--- /dev/null
+++ b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml
@@ -0,0 +1,4 @@
+---
+title: Narrow environment payload by using basic project details resource
+merge_request:
+author:
diff --git a/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
new file mode 100644
index 00000000000..bff996172f3
--- /dev/null
+++ b/changelogs/unreleased/28655-current-path-text-is-not-updated-after-setting-the-new-username.yml
@@ -0,0 +1,4 @@
+---
+title: Update account view to display new username
+merge_request:
+author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
new file mode 100644
index 00000000000..8b592766bf3
--- /dev/null
+++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dismissable error close is not visible enough
+merge_request: 9516
+author:
diff --git a/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
new file mode 100644
index 00000000000..e38e5d0db5b
--- /dev/null
+++ b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Improve grammar in GitLab flow documentation
+merge_request: 9552
+author: infogrind
diff --git a/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml
new file mode 100644
index 00000000000..b8dba0b5993
--- /dev/null
+++ b/changelogs/unreleased/28704-fullscreen-zen-mode-is-broken.yml
@@ -0,0 +1,4 @@
+---
+title: Set max height to screen height for Zen mode
+merge_request: 9667
+author:
diff --git a/changelogs/unreleased/28723-consistent-handling-indexof.yml b/changelogs/unreleased/28723-consistent-handling-indexof.yml
new file mode 100644
index 00000000000..95d6181d5fa
--- /dev/null
+++ b/changelogs/unreleased/28723-consistent-handling-indexof.yml
@@ -0,0 +1,4 @@
+---
+title: Keep consistent in handling indexOf results
+merge_request: 9531
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml
new file mode 100644
index 00000000000..38ff6b97b2b
--- /dev/null
+++ b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure archive download is only one directory deep
+merge_request: 9616
+author:
diff --git a/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml
new file mode 100644
index 00000000000..0016253e32e
--- /dev/null
+++ b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml
@@ -0,0 +1,4 @@
+---
+title: Enable filtering milestones by search criteria in the API
+merge_request: 9606
+author:
diff --git a/changelogs/unreleased/28835-jobs-head.yml b/changelogs/unreleased/28835-jobs-head.yml
new file mode 100644
index 00000000000..1580cfb19ba
--- /dev/null
+++ b/changelogs/unreleased/28835-jobs-head.yml
@@ -0,0 +1,4 @@
+---
+title: Fix jobs table header height
+merge_request:
+author:
diff --git a/changelogs/unreleased/28837-remove-help-duplicate.yml b/changelogs/unreleased/28837-remove-help-duplicate.yml
new file mode 100644
index 00000000000..b1001245663
--- /dev/null
+++ b/changelogs/unreleased/28837-remove-help-duplicate.yml
@@ -0,0 +1,4 @@
+---
+title: Remove help link from right dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml
new file mode 100644
index 00000000000..7c64783cbd0
--- /dev/null
+++ b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml
@@ -0,0 +1,4 @@
+---
+title: Add filter param for project membership for current_user in API v4
+merge_request:
+author:
diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
new file mode 100644
index 00000000000..0177394aa0f
--- /dev/null
+++ b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml
@@ -0,0 +1,4 @@
+---
+title: Order milestone issues by position ascending in api
+merge_request: 9635
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
new file mode 100644
index 00000000000..114a14ec2df
--- /dev/null
+++ b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge request even if target branch is not specified in query
+ params
+merge_request: 9968
+author:
diff --git a/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml
new file mode 100644
index 00000000000..9ba33af010c
--- /dev/null
+++ b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml
@@ -0,0 +1,4 @@
+---
+title: Highlight line number if specified on diff pages when page loads
+merge_request: 9664
+author:
diff --git a/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
new file mode 100644
index 00000000000..48e62f8f70d
--- /dev/null
+++ b/changelogs/unreleased/28898-fix-search-branches-in-cherry-picking.yml
@@ -0,0 +1,4 @@
+---
+title: Fix json response in branches controller
+merge_request: 9710
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28935-make-logo-smaller.yml b/changelogs/unreleased/28935-make-logo-smaller.yml
new file mode 100644
index 00000000000..ef79fc7d212
--- /dev/null
+++ b/changelogs/unreleased/28935-make-logo-smaller.yml
@@ -0,0 +1,4 @@
+---
+title: Decrease tanuki logo size
+merge_request:
+author:
diff --git a/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
new file mode 100644
index 00000000000..26989c14958
--- /dev/null
+++ b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
@@ -0,0 +1,4 @@
+---
+title: When viewing old wiki page version, edit button should be disabled
+merge_request: 9966
+author: TM Lee
diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
new file mode 100644
index 00000000000..f869249c22b
--- /dev/null
+++ b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml
@@ -0,0 +1,4 @@
+---
+title: Fix create issue form buttons are misaligned on mobile
+merge_request: 9706
+author: TM Lee
diff --git a/changelogs/unreleased/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml
new file mode 100644
index 00000000000..6d08db3d55d
--- /dev/null
+++ b/changelogs/unreleased/29034-fix-github-importer.yml
@@ -0,0 +1,4 @@
+---
+title: Fix name colision when importing GitHub pull requests from forked repositories
+merge_request: 9719
+author:
diff --git a/changelogs/unreleased/29046-fix-github-importer-open-prs.yml b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
new file mode 100644
index 00000000000..d279c269f94
--- /dev/null
+++ b/changelogs/unreleased/29046-fix-github-importer-open-prs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix GitHub Import deleting branches for open PRs from a fork
+merge_request: 9758
+author:
diff --git a/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
new file mode 100644
index 00000000000..0de7754badc
--- /dev/null
+++ b/changelogs/unreleased/29137-bulk-perform-async-should-specify-queue.yml
@@ -0,0 +1,4 @@
+---
+title: Make authorized projects worker use a specific queue instead of the default one
+merge_request: 9813
+author:
diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
new file mode 100644
index 00000000000..ad0c513f525
--- /dev/null
+++ b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_milestone_spec.rb
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29189-discussion-button.yml b/changelogs/unreleased/29189-discussion-button.yml
new file mode 100644
index 00000000000..eea96362117
--- /dev/null
+++ b/changelogs/unreleased/29189-discussion-button.yml
@@ -0,0 +1,4 @@
+---
+title: Fix alignment of resolve button
+merge_request:
+author:
diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml
new file mode 100644
index 00000000000..e8e3a71f875
--- /dev/null
+++ b/changelogs/unreleased/29209-sign-up-form-name.yml
@@ -0,0 +1,4 @@
+---
+title: Change label for name on sign up form
+merge_request:
+author:
diff --git a/changelogs/unreleased/29263-merge-button-color.yml b/changelogs/unreleased/29263-merge-button-color.yml
new file mode 100644
index 00000000000..2d0625483a4
--- /dev/null
+++ b/changelogs/unreleased/29263-merge-button-color.yml
@@ -0,0 +1,4 @@
+---
+title: ensure MR widget dropdown is same color as button
+merge_request:
+author:
diff --git a/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
new file mode 100644
index 00000000000..dabf9968c5b
--- /dev/null
+++ b/changelogs/unreleased/29328-fix-transient-failure-in-model-user-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Add custom attributes in factories
+merge_request: 9892
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
new file mode 100644
index 00000000000..85be36f7902
--- /dev/null
+++ b/changelogs/unreleased/29405-fix-project-wiki-update.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Project Wiki update
+merge_request: 9990
+author: Dongqing Hu
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
new file mode 100644
index 00000000000..61ffb64fa8f
--- /dev/null
+++ b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trigger webhook for ref with a dot
+merge_request: 10001
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
new file mode 100644
index 00000000000..23a32d2c11a
--- /dev/null
+++ b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Display full project name with namespace upon deletion
+merge_request:
+author:
diff --git a/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
new file mode 100644
index 00000000000..d0a04b0a130
--- /dev/null
+++ b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
@@ -0,0 +1,4 @@
+---
+title: Fix archive prefix bug for refs containing dots
+merge_request:
+author:
diff --git a/changelogs/unreleased/29604-v3-fix-branch-creation.yml b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
new file mode 100644
index 00000000000..25687e8be97
--- /dev/null
+++ b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
@@ -0,0 +1,4 @@
+---
+title: Use "branch_name" instead "branch" on V3 branch creation API
+merge_request:
+author:
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
new file mode 100644
index 00000000000..15d7b9dcafb
--- /dev/null
+++ b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to some Branch API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/3440-remove-hsts-header.yml b/changelogs/unreleased/3440-remove-hsts-header.yml
new file mode 100644
index 00000000000..0310e733f4e
--- /dev/null
+++ b/changelogs/unreleased/3440-remove-hsts-header.yml
@@ -0,0 +1,4 @@
+---
+title: Stop setting Strict-Transport-Securty header from within the app
+merge_request:
+author:
diff --git a/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
new file mode 100644
index 00000000000..4a4932288b4
--- /dev/null
+++ b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
@@ -0,0 +1,4 @@
+---
+title: Return 202 with JSON body on async removals on V4 API
+merge_request:
+author:
diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
deleted file mode 100644
index 11d1f55172b..00000000000
--- a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix notifications when set at group level
-merge_request: 6813
-author: Alexandre Maia
diff --git a/changelogs/unreleased/6073_project_api.yml b/changelogs/unreleased/6073_project_api.yml
new file mode 100644
index 00000000000..fd6792a406e
--- /dev/null
+++ b/changelogs/unreleased/6073_project_api.yml
@@ -0,0 +1,4 @@
+---
+title: 'API project create: Make name or path required'
+merge_request: 9416
+author:
diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml
deleted file mode 100644
index 75502e139e7..00000000000
--- a/changelogs/unreleased/8-15-stable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure export files are removed after a namespace is deleted
-merge_request:
-author:
diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml
deleted file mode 100644
index 136d2108c63..00000000000
--- a/changelogs/unreleased/8082-permalink-to-file.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add `y` keyboard shortcut to move to file permalink
-merge_request:
-author:
diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml
deleted file mode 100644
index 2f0f1887257..00000000000
--- a/changelogs/unreleased/9-0-api-changes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
-merge_request: 8967
-author:
diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
deleted file mode 100644
index 9fd6ea5bc52..00000000000
--- a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds /target_branch slash command functionality for merge requests
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
new file mode 100644
index 00000000000..307b7ec7359
--- /dev/null
+++ b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent more than one issue tracker to be active for the same project
+merge_request:
+author: luisdgs19
diff --git a/changelogs/unreleased/add-blob-copy-button.yml b/changelogs/unreleased/add-blob-copy-button.yml
new file mode 100644
index 00000000000..946723e523b
--- /dev/null
+++ b/changelogs/unreleased/add-blob-copy-button.yml
@@ -0,0 +1,4 @@
+---
+title: Add copy button to blob header and use icon for Raw button
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
new file mode 100644
index 00000000000..d10e4cb7c87
--- /dev/null
+++ b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
@@ -0,0 +1,4 @@
+---
+title: Add filtered search visual tokens
+merge_request: 8969
+author:
diff --git a/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml
new file mode 100644
index 00000000000..66d5bb63734
--- /dev/null
+++ b/changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml
@@ -0,0 +1,4 @@
+---
+title: Add frequently used emojis back to awards menu
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-git-version-to-system-info.yml b/changelogs/unreleased/add-git-version-to-system-info.yml
new file mode 100644
index 00000000000..2827fcec28d
--- /dev/null
+++ b/changelogs/unreleased/add-git-version-to-system-info.yml
@@ -0,0 +1,4 @@
+---
+title: Add git version to gitlab:env:info
+merge_request: 9128
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml
new file mode 100644
index 00000000000..1ae1e3c7a7a
--- /dev/null
+++ b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml
@@ -0,0 +1,4 @@
+---
+title: Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM
+merge_request: 9398
+author:
diff --git a/changelogs/unreleased/add-labels-to-issue-hook.yml b/changelogs/unreleased/add-labels-to-issue-hook.yml
new file mode 100644
index 00000000000..967430ee09f
--- /dev/null
+++ b/changelogs/unreleased/add-labels-to-issue-hook.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels array to the issue web hook returned object
+merge_request: 9972
+author:
diff --git a/changelogs/unreleased/add-pipeline-triggers.yml b/changelogs/unreleased/add-pipeline-triggers.yml
new file mode 100644
index 00000000000..81b11da0bb2
--- /dev/null
+++ b/changelogs/unreleased/add-pipeline-triggers.yml
@@ -0,0 +1,4 @@
+---
+title: Add pipeline trigger API with user permissions
+merge_request: 9277
+author:
diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml
new file mode 100644
index 00000000000..8087aa6296c
--- /dev/null
+++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml
@@ -0,0 +1,4 @@
+---
+title: Add merge request count to each issue on issues list
+merge_request: 9252
+author: blackst0ne
diff --git a/changelogs/unreleased/add_project_update_hook.yml b/changelogs/unreleased/add_project_update_hook.yml
deleted file mode 100644
index 915c9538843..00000000000
--- a/changelogs/unreleased/add_project_update_hook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add system hook for when a project is updated (other than rename/transfer)
-merge_request: 5711
-author: Tommy Beadle
diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
new file mode 100644
index 00000000000..088f1335796
--- /dev/null
+++ b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
@@ -0,0 +1,4 @@
+---
+title: Add quick submit for snippet forms
+merge_request: 9911
+author: blackst0ne
diff --git a/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
new file mode 100644
index 00000000000..c3c877423ff
--- /dev/null
+++ b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
@@ -0,0 +1,4 @@
+---
+title: Fix conflict resolution when files contain valid UTF-8 characters
+merge_request:
+author:
diff --git a/changelogs/unreleased/api-drop-subscribed.yml b/changelogs/unreleased/api-drop-subscribed.yml
new file mode 100644
index 00000000000..2a39026b519
--- /dev/null
+++ b/changelogs/unreleased/api-drop-subscribed.yml
@@ -0,0 +1,5 @@
+---
+title: Remove "subscribed" field from API responses returning list of issues or merge
+ requests
+merge_request: 9661
+author:
diff --git a/changelogs/unreleased/api-empty-return.yml b/changelogs/unreleased/api-empty-return.yml
new file mode 100644
index 00000000000..7810e83eb0e
--- /dev/null
+++ b/changelogs/unreleased/api-empty-return.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 204 for all delete endpoints'
+merge_request: 9397
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-notes-entity-fields.yml b/changelogs/unreleased/api-notes-entity-fields.yml
new file mode 100644
index 00000000000..f7631df31e2
--- /dev/null
+++ b/changelogs/unreleased/api-notes-entity-fields.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated fields Notes#upvotes and Notes#downvotes'
+merge_request: 9384
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-project-issues-404.yml b/changelogs/unreleased/api-project-issues-404.yml
new file mode 100644
index 00000000000..ce40395c99e
--- /dev/null
+++ b/changelogs/unreleased/api-project-issues-404.yml
@@ -0,0 +1,4 @@
+---
+title: Return 404 in project issues API endpoint when project cannot be found
+merge_request: 10093
+author:
diff --git a/changelogs/unreleased/api-remove-owned-groups.yml b/changelogs/unreleased/api-remove-owned-groups.yml
new file mode 100644
index 00000000000..cf0301b7fe0
--- /dev/null
+++ b/changelogs/unreleased/api-remove-owned-groups.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove /groups/owned endpoint'
+merge_request: 9505
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
deleted file mode 100644
index 67603bfab3b..00000000000
--- a/changelogs/unreleased/api-remove-snippets-expires-at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove deprecated ''expires_at'' from project snippets'
-merge_request: 8723
-author: Robert Schilling
diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml
deleted file mode 100644
index fda1c3bd562..00000000000
--- a/changelogs/unreleased/babel-all-the-things.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: use babel to transpile all non-vendor javascript assets regardless of file
- extension
-merge_request: 8988
-author:
diff --git a/changelogs/unreleased/backup_storage_class.yml b/changelogs/unreleased/backup_storage_class.yml
new file mode 100644
index 00000000000..fc9989fc251
--- /dev/null
+++ b/changelogs/unreleased/backup_storage_class.yml
@@ -0,0 +1,4 @@
+---
+title: Add storage class configuration option for Amazon S3 remote backups
+merge_request:
+author: Jon Keys
diff --git a/changelogs/unreleased/better-priority-sorting-2.yml b/changelogs/unreleased/better-priority-sorting-2.yml
new file mode 100644
index 00000000000..ca0d14718dc
--- /dev/null
+++ b/changelogs/unreleased/better-priority-sorting-2.yml
@@ -0,0 +1,4 @@
+---
+title: Allow filtering by all started milestones
+merge_request:
+author:
diff --git a/changelogs/unreleased/better-priority-sorting.yml b/changelogs/unreleased/better-priority-sorting.yml
new file mode 100644
index 00000000000..a44cd090ceb
--- /dev/null
+++ b/changelogs/unreleased/better-priority-sorting.yml
@@ -0,0 +1,4 @@
+---
+title: Allow sorting by due date and priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/branch_deletion.yml b/changelogs/unreleased/branch_deletion.yml
new file mode 100644
index 00000000000..dbc9265a1fb
--- /dev/null
+++ b/changelogs/unreleased/branch_deletion.yml
@@ -0,0 +1,4 @@
+---
+title: on branch deletion show loading icon and disabled the button
+merge_request: 6761
+author: wendy0402
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
deleted file mode 100644
index 77750b55e7e..00000000000
--- a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide version check image if there is no internet connection
-merge_request: 8355
-author: Ken Ding
diff --git a/changelogs/unreleased/bugfix-systemhook.yml b/changelogs/unreleased/bugfix-systemhook.yml
new file mode 100644
index 00000000000..4c4d0dcc7a2
--- /dev/null
+++ b/changelogs/unreleased/bugfix-systemhook.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bug when system hook for deploy key
+merge_request: 9796
+author: billy.lb
diff --git a/changelogs/unreleased/change_queue_weight.yml b/changelogs/unreleased/change_queue_weight.yml
deleted file mode 100644
index e4c650e8f79..00000000000
--- a/changelogs/unreleased/change_queue_weight.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Increase process_commit queue weight from 2 to 3
-merge_request: 9326
-author: blackst0ne
diff --git a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
new file mode 100644
index 00000000000..dc315ca2367
--- /dev/null
+++ b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Added remaining_time method to milestoneish, specs and updated the milestone_helper
+ milestone_remaining_days method to correctly return the correct remaining time.
+merge_request:
+author: Michael Robinson
diff --git a/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
new file mode 100644
index 00000000000..8778fac6e9d
--- /dev/null
+++ b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Clear ActiveRecord connections before starting Sidekiq
+merge_request:
+author:
diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml
deleted file mode 100644
index 6aa4a5664e7..00000000000
--- a/changelogs/unreleased/clipboard-button-commit-sha.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: 'Copy commit SHA to clipboard'
-merge_request: 8547
diff --git a/changelogs/unreleased/commit-search-ui-fix.yml b/changelogs/unreleased/commit-search-ui-fix.yml
deleted file mode 100644
index 4a5c2cf6090..00000000000
--- a/changelogs/unreleased/commit-search-ui-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed commit search UI
-merge_request:
-author:
diff --git a/changelogs/unreleased/commons-chunk-plugin.yml b/changelogs/unreleased/commons-chunk-plugin.yml
new file mode 100644
index 00000000000..5c11ea3bbb2
--- /dev/null
+++ b/changelogs/unreleased/commons-chunk-plugin.yml
@@ -0,0 +1,5 @@
+---
+title: Use webpack CommonsChunkPlugin to place common javascript libraries in their
+ own bundles
+merge_request: 9647
+author:
diff --git a/changelogs/unreleased/contribution-calendar-scroll.yml b/changelogs/unreleased/contribution-calendar-scroll.yml
deleted file mode 100644
index a504d59e61c..00000000000
--- a/changelogs/unreleased/contribution-calendar-scroll.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: contribution calendar scrolls from right to left
-merge_request:
-author:
diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml
deleted file mode 100644
index 506815a5b54..00000000000
--- a/changelogs/unreleased/cop-gem-fetcher.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Cop for gem fetched from a git source
-merge_request: 8856
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/copy-as-md.yml b/changelogs/unreleased/copy-as-md.yml
deleted file mode 100644
index 637e9dc36e2..00000000000
--- a/changelogs/unreleased/copy-as-md.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
-merge_request:
-author:
diff --git a/changelogs/unreleased/create_branch_repo_less.yml b/changelogs/unreleased/create_branch_repo_less.yml
new file mode 100644
index 00000000000..e8b14fa3b67
--- /dev/null
+++ b/changelogs/unreleased/create_branch_repo_less.yml
@@ -0,0 +1,4 @@
+---
+title: Creating a new branch from an issue will automatically initialize a repository if one doesn't already exist.
+merge_request:
+author:
diff --git a/changelogs/unreleased/dashboard-filter-search-keep-params.yml b/changelogs/unreleased/dashboard-filter-search-keep-params.yml
new file mode 100644
index 00000000000..a140715b7a2
--- /dev/null
+++ b/changelogs/unreleased/dashboard-filter-search-keep-params.yml
@@ -0,0 +1,4 @@
+---
+title: Dashboard project search keeps selected sort & filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/delete-artifacts-for-pages.yml b/changelogs/unreleased/delete-artifacts-for-pages.yml
new file mode 100644
index 00000000000..50b3dd81d60
--- /dev/null
+++ b/changelogs/unreleased/delete-artifacts-for-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Delete artifacts for pages unless expiry date is specified
+merge_request: 9716
+author:
diff --git a/changelogs/unreleased/diff-make-obvious-cant-comment.yml b/changelogs/unreleased/diff-make-obvious-cant-comment.yml
new file mode 100644
index 00000000000..2cb95947939
--- /dev/null
+++ b/changelogs/unreleased/diff-make-obvious-cant-comment.yml
@@ -0,0 +1,4 @@
+---
+title: Visually show expanded diff lines cant have comments
+merge_request:
+author:
diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
deleted file mode 100644
index 6dd0d748001..00000000000
--- a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable automatic login after clicking email confirmation links
-merge_request: 7472
-author:
diff --git a/changelogs/unreleased/display-project-id.yml b/changelogs/unreleased/display-project-id.yml
deleted file mode 100644
index 8705ed28400..00000000000
--- a/changelogs/unreleased/display-project-id.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display project ID in project settings
-merge_request: 8572
-author: winniehell
diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml
new file mode 100644
index 00000000000..15ae2da44a3
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Copy code as GFM from diffs, blobs and GFM code blocks
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-group-reference-full-name.yml b/changelogs/unreleased/dm-group-reference-full-name.yml
new file mode 100644
index 00000000000..f445d955529
--- /dev/null
+++ b/changelogs/unreleased/dm-group-reference-full-name.yml
@@ -0,0 +1,4 @@
+---
+title: Use full group name in GFM group reference title
+merge_request:
+author:
diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml
deleted file mode 100644
index 863e41b6413..00000000000
--- a/changelogs/unreleased/document-how-to-vue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds documentation for how to use Vue.js
-merge_request: 8866
-author:
diff --git a/changelogs/unreleased/dynamic-todos-fixture.yml b/changelogs/unreleased/dynamic-todos-fixture.yml
deleted file mode 100644
index 580bc729e3c..00000000000
--- a/changelogs/unreleased/dynamic-todos-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for right_sidebar_spec.js
-merge_request: 9211
-author: winniehell
diff --git a/changelogs/unreleased/dz-blacklist--names.yml b/changelogs/unreleased/dz-blacklist--names.yml
new file mode 100644
index 00000000000..2941965002d
--- /dev/null
+++ b/changelogs/unreleased/dz-blacklist--names.yml
@@ -0,0 +1,4 @@
+---
+title: Reserve few project and nested group paths that have wildcard routes associated
+merge_request: 9898
+author:
diff --git a/changelogs/unreleased/dz-change-project-view.yml b/changelogs/unreleased/dz-change-project-view.yml
new file mode 100644
index 00000000000..47e007a80a8
--- /dev/null
+++ b/changelogs/unreleased/dz-change-project-view.yml
@@ -0,0 +1,4 @@
+---
+title: Change default project view for user from readme to files view
+merge_request: 9584
+author:
diff --git a/changelogs/unreleased/dz-dashboard-groups-search.yml b/changelogs/unreleased/dz-dashboard-groups-search.yml
new file mode 100644
index 00000000000..c473cba774d
--- /dev/null
+++ b/changelogs/unreleased/dz-dashboard-groups-search.yml
@@ -0,0 +1,4 @@
+---
+title: Add filter and sorting to dashboard groups page
+merge_request: 9619
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
deleted file mode 100644
index 8e4eb7f1fff..00000000000
--- a/changelogs/unreleased/dz-nested-groups-improvements-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add read-only full_path and full_name attributes to Group API
-merge_request: 8827
-author:
diff --git a/changelogs/unreleased/dz-nested-groups-members.yml b/changelogs/unreleased/dz-nested-groups-members.yml
new file mode 100644
index 00000000000..bab0c8465c2
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-members.yml
@@ -0,0 +1,4 @@
+---
+title: Show members of parent groups on project members page
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-restrictions.yml b/changelogs/unreleased/dz-nested-groups-restrictions.yml
new file mode 100644
index 00000000000..2ffb6032525
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-restrictions.yml
@@ -0,0 +1,4 @@
+---
+title: Restrict nested group names to prevent ambiguous routes
+merge_request: 9738
+author:
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
deleted file mode 100644
index 5a42c98a800..00000000000
--- a/changelogs/unreleased/empty-selection-reply-shortcut.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change the reply shortcut to focus the field even without a selection.
-merge_request: 8873
-author: Brian Hall
diff --git a/changelogs/unreleased/enable-snippets-by-default.yml b/changelogs/unreleased/enable-snippets-by-default.yml
new file mode 100644
index 00000000000..04fa3f7bdae
--- /dev/null
+++ b/changelogs/unreleased/enable-snippets-by-default.yml
@@ -0,0 +1,4 @@
+---
+title: Enable snippets for new projects by default
+merge_request:
+author:
diff --git a/changelogs/unreleased/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml
new file mode 100644
index 00000000000..9d1c3ac7421
--- /dev/null
+++ b/changelogs/unreleased/es6-class-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Convert Issue into ES6 class
+merge_request: 9636
+author: winniehell
diff --git a/changelogs/unreleased/etag-notes-polling.yml b/changelogs/unreleased/etag-notes-polling.yml
new file mode 100644
index 00000000000..53990821d25
--- /dev/null
+++ b/changelogs/unreleased/etag-notes-polling.yml
@@ -0,0 +1,4 @@
+---
+title: Use ETag to improve performance of issue notes polling
+merge_request: 9036
+author:
diff --git a/changelogs/unreleased/expose-pagination-headers.yml b/changelogs/unreleased/expose-pagination-headers.yml
new file mode 100644
index 00000000000..1b4cd43fa06
--- /dev/null
+++ b/changelogs/unreleased/expose-pagination-headers.yml
@@ -0,0 +1,4 @@
+---
+title: 'CORS: Whitelist pagination headers'
+merge_request: 9651
+author: Robert Schilling
diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml
deleted file mode 100644
index b5cc6bbf8b6..00000000000
--- a/changelogs/unreleased/fe-commit-mr-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use vue.js Pipelines table in commit and merge request view
-merge_request: 8844
-author:
diff --git a/changelogs/unreleased/feature-brand-logo-in-emails.yml b/changelogs/unreleased/feature-brand-logo-in-emails.yml
new file mode 100644
index 00000000000..a7674b9b25e
--- /dev/null
+++ b/changelogs/unreleased/feature-brand-logo-in-emails.yml
@@ -0,0 +1,4 @@
+---
+title: Brand header logo for pipeline emails
+merge_request: 9049
+author: Alexis Reigel
diff --git a/changelogs/unreleased/feature-custom-lfs.yml b/changelogs/unreleased/feature-custom-lfs.yml
new file mode 100644
index 00000000000..ec968386a6f
--- /dev/null
+++ b/changelogs/unreleased/feature-custom-lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Do not show LFS object when LFS is disabled
+merge_request: 9779
+author: Christopher Bartz
diff --git a/changelogs/unreleased/feature-openid-connect.yml b/changelogs/unreleased/feature-openid-connect.yml
new file mode 100644
index 00000000000..e84eb7aff86
--- /dev/null
+++ b/changelogs/unreleased/feature-openid-connect.yml
@@ -0,0 +1,4 @@
+---
+title: Implement OpenID Connect identity provider
+merge_request: 8018
+author: Markus Koller
diff --git a/changelogs/unreleased/feature-runner-jobs-v4-api.yml b/changelogs/unreleased/feature-runner-jobs-v4-api.yml
new file mode 100644
index 00000000000..b24ea65266d
--- /dev/null
+++ b/changelogs/unreleased/feature-runner-jobs-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's jobs v4 API
+merge_request: 9273
+author:
diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
new file mode 100644
index 00000000000..e646a6a17b7
--- /dev/null
+++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's registration/deletion v4 API
+merge_request: 9246
+author:
diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
deleted file mode 100644
index 5fba0332881..00000000000
--- a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use warning icon in mini-graph if stage passed conditionally
-merge_request: 8503
-author:
diff --git a/changelogs/unreleased/feature-syshook_commits.yml b/changelogs/unreleased/feature-syshook_commits.yml
new file mode 100644
index 00000000000..1305f5cd414
--- /dev/null
+++ b/changelogs/unreleased/feature-syshook_commits.yml
@@ -0,0 +1,4 @@
+---
+title: Added commit array to Syshook json
+merge_request: 9685
+author: Gabriele Pongelli
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
new file mode 100644
index 00000000000..4b668d994a1
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for CommitController#show
+merge_request: 9629
+author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
deleted file mode 100644
index cc72a830695..00000000000
--- a/changelogs/unreleased/fix-27479.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove new branch button for confidential issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-29093.yml b/changelogs/unreleased/fix-29093.yml
new file mode 100644
index 00000000000..791129afe93
--- /dev/null
+++ b/changelogs/unreleased/fix-29093.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml
deleted file mode 100644
index 33b677b1f29..00000000000
--- a/changelogs/unreleased/fix-api-mr-permissions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't allow project guests to subscribe to merge requests through the API
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/fix-ar-connection-leaks.yml b/changelogs/unreleased/fix-ar-connection-leaks.yml
deleted file mode 100644
index 9da715560ad..00000000000
--- a/changelogs/unreleased/fix-ar-connection-leaks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't connect in Gitlab::Database.adapter_name
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml
deleted file mode 100644
index 26003713ed4..00000000000
--- a/changelogs/unreleased/fix-ci-build-policy.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve build policy and access abilities
-merge_request: 8711
-author:
diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml
deleted file mode 100644
index e13215f22a7..00000000000
--- a/changelogs/unreleased/fix-deleting-project-again.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix deleting projects with pipelines and builds
-merge_request: 8960
-author:
diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml
deleted file mode 100644
index 61817027720..00000000000
--- a/changelogs/unreleased/fix-depr-warn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: resolve deprecation warnings
-merge_request: 8855
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
deleted file mode 100644
index df7e3776700..00000000000
--- a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context
-merge_request: 8981
-author:
diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
new file mode 100644
index 00000000000..4db684c40b2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve project pipeline status caching problem on dashboard
+merge_request: 9895
+author:
diff --git a/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
new file mode 100644
index 00000000000..605b5f01d0e
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
@@ -0,0 +1,4 @@
+---
+title: Deprecate usage of `types` configuration entry to describe CI/CD stages
+merge_request: 9766
+author:
diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
new file mode 100644
index 00000000000..6365b1a1910
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml
@@ -0,0 +1,4 @@
+---
+title: Fix "passed with warnings" stage status on MySQL installations
+merge_request: 9802
+author:
diff --git a/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml
new file mode 100644
index 00000000000..71ff768a190
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml
@@ -0,0 +1,4 @@
+---
+title: Remove deprecated build status badge and related services
+merge_request: 9620
+author:
diff --git a/changelogs/unreleased/fix-gb-update-commit-status-api.yml b/changelogs/unreleased/fix-gb-update-commit-status-api.yml
new file mode 100644
index 00000000000..aa4fcba4e89
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-update-commit-status-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix updaing commit status when using optional attributes
+merge_request: 9618
+author:
diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
deleted file mode 100644
index 81377c0c6f0..00000000000
--- a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from creating notes on resources they can't access
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml
deleted file mode 100644
index e34d895570b..00000000000
--- a/changelogs/unreleased/fix-import-encrypt-atts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ignore encrypted attributes in Import/Export
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml
deleted file mode 100644
index fe580af31b3..00000000000
--- a/changelogs/unreleased/fix-import-group-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to export project inherited group members to Import/Export
-merge_request: 8923
-author:
diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
deleted file mode 100644
index d5f34b4b25d..00000000000
--- a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix job to pipeline renaming
-merge_request: 9147
-author:
diff --git a/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml
new file mode 100644
index 00000000000..ee827b7c939
--- /dev/null
+++ b/changelogs/unreleased/fix-mentioned-issues-for-external-trackers.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issues mentioned but not closed for external issue trackers
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml
new file mode 100644
index 00000000000..bf17a758c80
--- /dev/null
+++ b/changelogs/unreleased/fix-milestone-name-on-show.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Milestone name on show page
+merge_request:
+author: Raveesh
diff --git a/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
new file mode 100644
index 00000000000..a42b0db3cfc
--- /dev/null
+++ b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
@@ -0,0 +1,4 @@
+---
+title: Removed d3 from the main application.js bundle
+merge_request: 10062
+author:
diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml
deleted file mode 100644
index b927279cdf4..00000000000
--- a/changelogs/unreleased/fix-references-header-parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix reply by email without sub-addressing for some clients from
- Microsoft and Apple
-merge_request: 8620
-author:
diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml
deleted file mode 100644
index e98ac755b88..00000000000
--- a/changelogs/unreleased/fix-scroll-test.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change rspec test to guarantee window is resized before visiting page
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-slow-queries-for-branches-index.yml b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
new file mode 100644
index 00000000000..f5bd7003615
--- /dev/null
+++ b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes n+1 query for tags and branches index page
+merge_request: 9905
+author:
diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
deleted file mode 100644
index c9edd1de86c..00000000000
--- a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from deleting system deploy keys via the project deploy key API
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
deleted file mode 100644
index 4551212759f..00000000000
--- a/changelogs/unreleased/fix_broken_diff_discussions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make MR-review-discussions more reliable
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml
deleted file mode 100644
index e09d03bb608..00000000000
--- a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix incorrect sidekiq concurrency count in admin background page
-merge_request:
-author: wendy0402
diff --git a/changelogs/unreleased/fix_updated_field_in_issues-atom.yml b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
new file mode 100644
index 00000000000..414facdf779
--- /dev/null
+++ b/changelogs/unreleased/fix_updated_field_in_issues-atom.yml
@@ -0,0 +1,4 @@
+---
+title: Fix xml.updated field in rss/atom feeds
+merge_request: 9889
+author: blackst0ne
diff --git a/changelogs/unreleased/fix_visibility_level.yml b/changelogs/unreleased/fix_visibility_level.yml
new file mode 100644
index 00000000000..4cf649124ca
--- /dev/null
+++ b/changelogs/unreleased/fix_visibility_level.yml
@@ -0,0 +1,4 @@
+---
+title: Fix visibility level on new project page
+merge_request: 9885
+author: blackst0ne
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
new file mode 100644
index 00000000000..f353400753a
--- /dev/null
+++ b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'Removes UJS from pipelines tables'
+merge_request: 9929
+author:
diff --git a/changelogs/unreleased/format-timeago-date.yml b/changelogs/unreleased/format-timeago-date.yml
new file mode 100644
index 00000000000..f331c34abbc
--- /dev/null
+++ b/changelogs/unreleased/format-timeago-date.yml
@@ -0,0 +1,4 @@
+---
+title: Format timeago date to short format
+merge_request:
+author:
diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml
deleted file mode 100644
index 1427e4e7624..00000000000
--- a/changelogs/unreleased/fwn-to-find-by-full-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: replace `find_with_namespace` with `find_by_full_path`
-merge_request: 8949
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml b/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
deleted file mode 100644
index f60417d185e..00000000000
--- a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make notification_service spec DRYer by making test reusable
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml
deleted file mode 100644
index b2959c02c07..00000000000
--- a/changelogs/unreleased/git_to_html_redirection.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redirect http://someproject.git to http://someproject
-merge_request:
-author: blackst0ne
diff --git a/changelogs/unreleased/gitaly-post-receive.yml b/changelogs/unreleased/gitaly-post-receive.yml
new file mode 100644
index 00000000000..cf206e39084
--- /dev/null
+++ b/changelogs/unreleased/gitaly-post-receive.yml
@@ -0,0 +1,4 @@
+---
+title: Add internal API to notify Gitaly of post receive
+merge_request: 8983
+author:
diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml
deleted file mode 100644
index 7f372ccb428..00000000000
--- a/changelogs/unreleased/go-go-gadget-webpack.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: use webpack to bundle frontend assets and use karma for frontend testing
-merge_request: 7288
-author:
diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
new file mode 100644
index 00000000000..aff1bdd957c
--- /dev/null
+++ b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved the gear settings dropdown to a tab in the groups view
+merge_request:
+author:
diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml
deleted file mode 100644
index c11c2d4ede1..00000000000
--- a/changelogs/unreleased/group-label-sidebar-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed group label links in issue/merge request sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/handle-failure-when-deleting-tags.yml b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
new file mode 100644
index 00000000000..99b07c5fb5f
--- /dev/null
+++ b/changelogs/unreleased/handle-failure-when-deleting-tags.yml
@@ -0,0 +1,4 @@
+---
+title: Display error message when deleting tag in web UI fails
+merge_request: 9906
+author:
diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml
deleted file mode 100644
index 1b0a63efa51..00000000000
--- a/changelogs/unreleased/hardcode-title-system-note.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure autogenerated title does not cause failing spec
-merge_request: 8963
-author: brian m. carlson
diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml
deleted file mode 100644
index 39a85e3d261..00000000000
--- a/changelogs/unreleased/improve-ci-example-php-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed composer installer script in the CI PHP example doc
-merge_request: 4342
-author: Jeffrey Cafferata
diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml
deleted file mode 100644
index 8ae3dfe079c..00000000000
--- a/changelogs/unreleased/improve-handleLocationHash-tests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve gl.utils.handleLocationHash tests
-merge_request:
-author:
diff --git a/changelogs/unreleased/introduce-pipeline-triggers.yml b/changelogs/unreleased/introduce-pipeline-triggers.yml
new file mode 100644
index 00000000000..ce5a230d48f
--- /dev/null
+++ b/changelogs/unreleased/introduce-pipeline-triggers.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce Pipeline Triggers that are user-aware
+merge_request:
+author:
diff --git a/changelogs/unreleased/issuable-sidebar-bug.yml b/changelogs/unreleased/issuable-sidebar-bug.yml
deleted file mode 100644
index 4086292eb89..00000000000
--- a/changelogs/unreleased/issuable-sidebar-bug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed Issuable sidebar not closing on smaller/mobile sized screens
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
deleted file mode 100644
index 60da1c14702..00000000000
--- a/changelogs/unreleased/issue-20428.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to define a coverage regex in the .gitlab-ci.yml
-merge_request: 7447
-author: Leandro Camargo
diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml
new file mode 100644
index 00000000000..b02be70c470
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-new-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Added new filtered search bar to issue boards
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-descrpiption-spinner-off.yml b/changelogs/unreleased/issue-descrpiption-spinner-off.yml
new file mode 100644
index 00000000000..87104d09804
--- /dev/null
+++ b/changelogs/unreleased/issue-descrpiption-spinner-off.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed loading spinner position on issue template toggle
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
deleted file mode 100644
index 263af75b9e9..00000000000
--- a/changelogs/unreleased/issue-sidebar-empty-assignee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resets assignee dropdown when sidebar is open
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml
new file mode 100644
index 00000000000..abf4a609932
--- /dev/null
+++ b/changelogs/unreleased/issue-tags-layout.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'New Tag' layout on Tags page
+merge_request:
+author: Robert Marcano
diff --git a/changelogs/unreleased/issue_16834.yml b/changelogs/unreleased/issue_16834.yml
new file mode 100644
index 00000000000..06175579ac3
--- /dev/null
+++ b/changelogs/unreleased/issue_16834.yml
@@ -0,0 +1,4 @@
+---
+title: Update API endpoints for raw files
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
deleted file mode 100644
index 5dea1493f23..00000000000
--- a/changelogs/unreleased/issue_19262.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disallow system notes for closed issuables
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_23317.yml b/changelogs/unreleased/issue_23317.yml
deleted file mode 100644
index 788ae159f5e..00000000000
--- a/changelogs/unreleased/issue_23317.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix timezone on issue boards due date
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml
new file mode 100644
index 00000000000..916e47d36a9
--- /dev/null
+++ b/changelogs/unreleased/issue_24815.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issuable stale object error handler for js when updating tasklists
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_25112.yml b/changelogs/unreleased/issue_25112.yml
deleted file mode 100644
index c43d2732b9a..00000000000
--- a/changelogs/unreleased/issue_25112.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable invalid service templates
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27211.yml b/changelogs/unreleased/issue_27211.yml
deleted file mode 100644
index ad48fec5d85..00000000000
--- a/changelogs/unreleased/issue_27211.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unused js response from refs controller
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
new file mode 100644
index 00000000000..7a7e04f7ca7
--- /dev/null
+++ b/changelogs/unreleased/issue_27212.yml
@@ -0,0 +1,4 @@
+---
+title: Add closed_at field to issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_28051_2.yml b/changelogs/unreleased/issue_28051_2.yml
deleted file mode 100644
index 8cc32ad8493..00000000000
--- a/changelogs/unreleased/issue_28051_2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use default branch as target_branch when parameter is missing
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_29449.yml b/changelogs/unreleased/issue_29449.yml
new file mode 100644
index 00000000000..3556f22b080
--- /dev/null
+++ b/changelogs/unreleased/issue_29449.yml
@@ -0,0 +1,4 @@
+---
+title: Remove whitespace in group links
+merge_request: 9947
+author: Xurxo Méndez Pérez
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
deleted file mode 100644
index ee4a43a93db..00000000000
--- a/changelogs/unreleased/jej-pages-picked-from-ee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added GitLab Pages to CE
-merge_request: 8463
-author:
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
deleted file mode 100644
index 2ab997bf420..00000000000
--- a/changelogs/unreleased/label-promotion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Project labels can now be promoted to group labels"
-merge_request: 7242
-author: Olaf Tomalka
diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml
deleted file mode 100644
index 60f62d7691b..00000000000
--- a/changelogs/unreleased/lfs-noauth-public-repo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support unauthenticated LFS object downloads for public projects
-merge_request: 8824
-author: Ben Boeckel
diff --git a/changelogs/unreleased/list_issues_with_no_labels.yml b/changelogs/unreleased/list_issues_with_no_labels.yml
new file mode 100644
index 00000000000..ab44841631b
--- /dev/null
+++ b/changelogs/unreleased/list_issues_with_no_labels.yml
@@ -0,0 +1,4 @@
+---
+title: Document ability to list issues with no labels using API
+merge_request: 9697
+author: Vignesh Ravichandran
diff --git a/changelogs/unreleased/long-file-name-overflow.yml b/changelogs/unreleased/long-file-name-overflow.yml
new file mode 100644
index 00000000000..7ccf05491e1
--- /dev/null
+++ b/changelogs/unreleased/long-file-name-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed long file names overflowing under action buttons
+merge_request:
+author:
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
new file mode 100644
index 00000000000..9b95e06954a
--- /dev/null
+++ b/changelogs/unreleased/make-karma-fast-again.yml
@@ -0,0 +1,4 @@
+---
+title: Only add code coverage instrumentation when generating coverage report
+merge_request: 9987
+author:
diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml
deleted file mode 100644
index c855f0cbcf7..00000000000
--- a/changelogs/unreleased/markdown-plantuml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: PlantUML support for Markdown
-merge_request: 8588
-author: Horacio Sanson
diff --git a/changelogs/unreleased/merge-request-tabs-fixture.yml b/changelogs/unreleased/merge-request-tabs-fixture.yml
deleted file mode 100644
index 289cd7b604a..00000000000
--- a/changelogs/unreleased/merge-request-tabs-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for merge_request_tabs_spec.js
-merge_request: 9172
-author: winniehell
diff --git a/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
new file mode 100644
index 00000000000..ce4d5092c17
--- /dev/null
+++ b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate SlackService and MattermostService from build_events to pipeline_events, and migrate BuildsEmailService to PipelinesEmailService. Update Hipchat to use pipeline events rather than build events.
+merge_request: 8196
+author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
deleted file mode 100644
index f32b3aea3c8..00000000000
--- a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: adds avatar for discussion note
-merge_request: 8734
-author:
diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml
new file mode 100644
index 00000000000..24c6366177f
--- /dev/null
+++ b/changelogs/unreleased/mock-ci-service.yml
@@ -0,0 +1,4 @@
+---
+title: Add Mock CI service/integration for development
+merge_request:
+author:
diff --git a/changelogs/unreleased/moving-issue-with-two-list-labels.yml b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
new file mode 100644
index 00000000000..d5ea81e3810
--- /dev/null
+++ b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Removes label when moving issue to another list that it is currently in
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-diff-comment-button.yml b/changelogs/unreleased/mr-diff-comment-button.yml
new file mode 100644
index 00000000000..1dc6ed1c495
--- /dev/null
+++ b/changelogs/unreleased/mr-diff-comment-button.yml
@@ -0,0 +1,4 @@
+---
+title: Improved diff comment button UX
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-tabs-container-offset.yml b/changelogs/unreleased/mr-tabs-container-offset.yml
deleted file mode 100644
index c5df8abfcf2..00000000000
--- a/changelogs/unreleased/mr-tabs-container-offset.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed merge requests tab extra margin when fixed to window
-merge_request:
-author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
deleted file mode 100644
index 5ce080b6912..00000000000
--- a/changelogs/unreleased/newline-eslint-rule.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Flag multiple empty lines in eslint, fix offenses.
-merge_request: 8137
-author:
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
deleted file mode 100644
index 09e0b3a12d8..00000000000
--- a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: dismiss sidebar on repo buttons click
-merge_request: 8798
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml
deleted file mode 100644
index 6106c027360..00000000000
--- a/changelogs/unreleased/no_project_notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support notes when a project is not specified (personal snippet notes)
-merge_request: 8468
-author:
diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
new file mode 100644
index 00000000000..e675ed945ad
--- /dev/null
+++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
@@ -0,0 +1,4 @@
+---
+title: Only create unmergeable todos once when MR fails to merge
+merge_request:
+author:
diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
deleted file mode 100644
index c2e0410cc33..00000000000
--- a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to receive email notifications about your own activity
-merge_request: 8836
-author: Richard Macklin
diff --git a/changelogs/unreleased/pages-0-4-0.yml b/changelogs/unreleased/pages-0-4-0.yml
new file mode 100644
index 00000000000..7286b25125e
--- /dev/null
+++ b/changelogs/unreleased/pages-0-4-0.yml
@@ -0,0 +1,4 @@
+---
+title: Use GitLab Pages v0.4.0
+merge_request: 9896
+author:
diff --git a/changelogs/unreleased/pipeline-blocking-actions.yml b/changelogs/unreleased/pipeline-blocking-actions.yml
new file mode 100644
index 00000000000..6bde501de18
--- /dev/null
+++ b/changelogs/unreleased/pipeline-blocking-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Make it possible to configure blocking manual actions
+merge_request: 9585
+author:
diff --git a/changelogs/unreleased/pipeline-tooltips-overflow.yml b/changelogs/unreleased/pipeline-tooltips-overflow.yml
new file mode 100644
index 00000000000..184da8049f3
--- /dev/null
+++ b/changelogs/unreleased/pipeline-tooltips-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed pipeline actions tooltips overflowing
+merge_request:
+author:
diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml
deleted file mode 100644
index c2fa1a7fad0..00000000000
--- a/changelogs/unreleased/pms-lowercase-system-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make all system notes lowercase
-merge_request: 8807
-author:
diff --git a/changelogs/unreleased/priority-to-label-priority.yml b/changelogs/unreleased/priority-to-label-priority.yml
new file mode 100644
index 00000000000..2d9c58bfd9b
--- /dev/null
+++ b/changelogs/unreleased/priority-to-label-priority.yml
@@ -0,0 +1,4 @@
+---
+title: Rename priority sorting option to label priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
deleted file mode 100644
index 547a7c6755c..00000000000
--- a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redesign searchbar in admin project list
-merge_request: 8776
-author:
diff --git a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
deleted file mode 100644
index e0f7e11b6d1..00000000000
--- a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Search feature: redirects to commit page if query is commit sha and only commit
- found'
-merge_request: 8028
-author: YarNayar
diff --git a/changelogs/unreleased/refresh-permissions-recent-users.yml b/changelogs/unreleased/refresh-permissions-recent-users.yml
new file mode 100644
index 00000000000..4d08be6ed5c
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-recent-users.yml
@@ -0,0 +1,4 @@
+---
+title: Reset users.authorized_projects_populated to automatically refresh user permissions
+merge_request:
+author:
diff --git a/changelogs/unreleased/relative-url-assets.yml b/changelogs/unreleased/relative-url-assets.yml
deleted file mode 100644
index 0877664aca4..00000000000
--- a/changelogs/unreleased/relative-url-assets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: allow relative url change without recompiling frontend assets
-merge_request: 8831
-author:
diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml
deleted file mode 100644
index 3ff69adb4d3..00000000000
--- a/changelogs/unreleased/remove-deploy-key-endpoint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove /projects/:id/keys/.. endpoints'
-merge_request: 8716
-author: Robert Schilling
diff --git a/changelogs/unreleased/remove-es6-extension.yml b/changelogs/unreleased/remove-es6-extension.yml
new file mode 100644
index 00000000000..65f4a7a7867
--- /dev/null
+++ b/changelogs/unreleased/remove-es6-extension.yml
@@ -0,0 +1,4 @@
+---
+title: Remove es6 file extension from JavaScript files
+merge_request: 9241
+author: winniehell
diff --git a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml
deleted file mode 100644
index b75b4644ba3..00000000000
--- a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove issue and MR counts from labels index
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml
new file mode 100644
index 00000000000..c768f702ba2
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml
@@ -0,0 +1,4 @@
+---
+title: Removed jQuery UI highlight & autocomplete
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-new-relic-gem.yml b/changelogs/unreleased/remove-new-relic-gem.yml
new file mode 100644
index 00000000000..b15ecd3e4e7
--- /dev/null
+++ b/changelogs/unreleased/remove-new-relic-gem.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the newrelic gem
+merge_request: 9622
+author: Robert Schilling
diff --git a/changelogs/unreleased/remove-readme-option.yml b/changelogs/unreleased/remove-readme-option.yml
new file mode 100644
index 00000000000..1d4c862c00e
--- /dev/null
+++ b/changelogs/unreleased/remove-readme-option.yml
@@ -0,0 +1,4 @@
+---
+title: Remove readme-only project view preference
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-subscribe-label-tooltip.yml b/changelogs/unreleased/remove-subscribe-label-tooltip.yml
new file mode 100644
index 00000000000..90b71d3be51
--- /dev/null
+++ b/changelogs/unreleased/remove-subscribe-label-tooltip.yml
@@ -0,0 +1,4 @@
+---
+title: Remove tooltips from label subscription buttons
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-unused-ci-tables.yml b/changelogs/unreleased/remove-unused-ci-tables.yml
new file mode 100644
index 00000000000..fccfb882bd9
--- /dev/null
+++ b/changelogs/unreleased/remove-unused-ci-tables.yml
@@ -0,0 +1,4 @@
+---
+title: Remove various unused CI tables and columns
+merge_request: 9639
+author:
diff --git a/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
new file mode 100644
index 00000000000..4067b3de00c
--- /dev/null
+++ b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename table ci_commits to ci_pipelines
+merge_request: 9638
+author:
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
new file mode 100644
index 00000000000..d3109bdb17e
--- /dev/null
+++ b/changelogs/unreleased/rename_all_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Rename 'All issues' to 'Open issues' in Add issues modal
+merge_request: 10042
+author: blackst0ne
diff --git a/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
new file mode 100644
index 00000000000..e799dd3b48d
--- /dev/null
+++ b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
@@ -0,0 +1,4 @@
+---
+title: Change project count limit from 10 to 100000
+merge_request:
+author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
deleted file mode 100644
index 9b6df0c54af..00000000000
--- a/changelogs/unreleased/route-map.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add 'View on [env]' link to blobs and individual files in diffs
-merge_request: 8867
-author:
diff --git a/changelogs/unreleased/routes-lower-case.yml b/changelogs/unreleased/routes-lower-case.yml
new file mode 100644
index 00000000000..2110956680c
--- /dev/null
+++ b/changelogs/unreleased/routes-lower-case.yml
@@ -0,0 +1,4 @@
+---
+title: Remove repeated routes.path check for postgresql database
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml
deleted file mode 100644
index c0c23fb6f11..00000000000
--- a/changelogs/unreleased/rs-warden-blocked-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't perform Devise trackable updates on blocked User records
-merge_request: 8915
-author:
diff --git a/changelogs/unreleased/rss-btn-alignment-fix.yml b/changelogs/unreleased/rss-btn-alignment-fix.yml
new file mode 100644
index 00000000000..c8f57ec0b7c
--- /dev/null
+++ b/changelogs/unreleased/rss-btn-alignment-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed RSS button alignment on activity pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/set-default-cache-key-for-jobs.yml b/changelogs/unreleased/set-default-cache-key-for-jobs.yml
new file mode 100644
index 00000000000..b69348d2ece
--- /dev/null
+++ b/changelogs/unreleased/set-default-cache-key-for-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Set default cache key to "default" for jobs
+merge_request: 9666
+author:
diff --git a/changelogs/unreleased/settings-tab.yml b/changelogs/unreleased/settings-tab.yml
new file mode 100644
index 00000000000..69990c9a917
--- /dev/null
+++ b/changelogs/unreleased/settings-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved project settings from the gear drop-down menu to a tab
+merge_request: 9786
+author:
diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
deleted file mode 100644
index bab76812a17..00000000000
--- a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add index to ci_trigger_requests for commit_id
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-add-labels-index.yml b/changelogs/unreleased/sh-add-labels-index.yml
deleted file mode 100644
index b948a75081c..00000000000
--- a/changelogs/unreleased/sh-add-labels-index.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add indices to improve loading of labels page
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
new file mode 100644
index 00000000000..57f1474093a
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
+merge_request:
+author:
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
new file mode 100644
index 00000000000..062626359ef
--- /dev/null
+++ b/changelogs/unreleased/simplify-docs-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify trigger_docs build job for CE and EE
+merge_request: 9820
+author: winniehell
diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml
deleted file mode 100644
index e6ffb94bd08..00000000000
--- a/changelogs/unreleased/slash-commands-typo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed "substract" typo on /help/user/project/slash_commands
-merge_request: 8976
-author: Jason Aquino
diff --git a/changelogs/unreleased/small-screen-fullscreen-button.yml b/changelogs/unreleased/small-screen-fullscreen-button.yml
deleted file mode 100644
index f4c269bc473..00000000000
--- a/changelogs/unreleased/small-screen-fullscreen-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display fullscreen button on small screens
-merge_request: 5302
-author: winniehell
diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml
deleted file mode 100644
index 2895478abfd..00000000000
--- a/changelogs/unreleased/snippets-search-performance.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduced query count for snippet search
-merge_request:
-author:
diff --git a/changelogs/unreleased/sort-builds-in-stage-dropdown.yml b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml
new file mode 100644
index 00000000000..646f25125b1
--- /dev/null
+++ b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Sort builds in stage dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssh-key-paste.yml b/changelogs/unreleased/ssh-key-paste.yml
new file mode 100644
index 00000000000..1e34ef60f6e
--- /dev/null
+++ b/changelogs/unreleased/ssh-key-paste.yml
@@ -0,0 +1,4 @@
+---
+title: SSH key field updates title after pasting key
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssrf-protections.yml b/changelogs/unreleased/ssrf-protections.yml
new file mode 100644
index 00000000000..8d803738009
--- /dev/null
+++ b/changelogs/unreleased/ssrf-protections.yml
@@ -0,0 +1,4 @@
+---
+title: To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-api-pipeline-jobs.yml b/changelogs/unreleased/tc-api-pipeline-jobs.yml
new file mode 100644
index 00000000000..993c1b6526a
--- /dev/null
+++ b/changelogs/unreleased/tc-api-pipeline-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Add GET /projects/:id/pipelines/:pipeline_id/jobs endpoint
+merge_request: 9727
+author:
diff --git a/changelogs/unreleased/tc-fix-project-create-500.yml b/changelogs/unreleased/tc-fix-project-create-500.yml
new file mode 100644
index 00000000000..1b746a41eab
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-project-create-500.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for creating a project through API when import_url is nil
+merge_request: 9841
+author:
diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
deleted file mode 100644
index a7f5dcb560c..00000000000
--- a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only show Merge Request button when user can create a MR
-merge_request: 8639
-author:
diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml
deleted file mode 100644
index db1e66770d1..00000000000
--- a/changelogs/unreleased/terminal-max-session-time.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce maximum session time for terminal websocket connection
-merge_request: 8413
-author:
diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml
new file mode 100644
index 00000000000..0a60b4d46a3
--- /dev/null
+++ b/changelogs/unreleased/unified-member-api-response.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 400 for all validation erros in the mebers API'
+merge_request: 9523
+author: Robert Schilling
diff --git a/changelogs/unreleased/update-ace.yml b/changelogs/unreleased/update-ace.yml
new file mode 100644
index 00000000000..dbe476e3ae0
--- /dev/null
+++ b/changelogs/unreleased/update-ace.yml
@@ -0,0 +1,4 @@
+---
+title: Update code editor (ACE) to 1.2.6, to fix input problems with compose key
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml
new file mode 100644
index 00000000000..acc42bf00b1
--- /dev/null
+++ b/changelogs/unreleased/update-vue-2-1.yml
@@ -0,0 +1,4 @@
+---
+title: update Vue to v2.1.10
+merge_request: 9386
+author:
diff --git a/changelogs/unreleased/updated-pages-0-3-1.yml b/changelogs/unreleased/updated-pages-0-3-1.yml
deleted file mode 100644
index 8622b823c86..00000000000
--- a/changelogs/unreleased/updated-pages-0-3-1.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update GitLab Pages to v0.3.1
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml
deleted file mode 100644
index 55f9b3e407c..00000000000
--- a/changelogs/unreleased/upgrade-babel-v6.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade babel 5.8.x to babel 6.22.x
-merge_request: 9072
-author:
diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml
deleted file mode 100644
index 7e0334566dc..00000000000
--- a/changelogs/unreleased/upgrade-omniauth.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Upgrade omniauth gem to 1.3.2
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml
deleted file mode 100644
index 6a49859d68c..00000000000
--- a/changelogs/unreleased/upgrade-webpack-v2-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade to webpack v2.2
-merge_request: 9078
-author:
diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml
new file mode 100644
index 00000000000..381f80c5c0d
--- /dev/null
+++ b/changelogs/unreleased/use-corejs-polyfills.yml
@@ -0,0 +1,4 @@
+---
+title: Standardize on core-js for es2015 polyfills
+merge_request: 9749
+author:
diff --git a/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml
new file mode 100644
index 00000000000..ff5a58f6232
--- /dev/null
+++ b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml
@@ -0,0 +1,4 @@
+---
+title: Use redis channel to post notifications
+merge_request:
+author:
diff --git a/changelogs/unreleased/user-calendar-border.yml b/changelogs/unreleased/user-calendar-border.yml
new file mode 100644
index 00000000000..8ebcca83256
--- /dev/null
+++ b/changelogs/unreleased/user-calendar-border.yml
@@ -0,0 +1,4 @@
+---
+title: Removed top border from user contribution calendar
+merge_request:
+author:
diff --git a/changelogs/unreleased/user-callouts.yml b/changelogs/unreleased/user-callouts.yml
new file mode 100644
index 00000000000..f6ce06a3d8f
--- /dev/null
+++ b/changelogs/unreleased/user-callouts.yml
@@ -0,0 +1,4 @@
+---
+title: Added user callouts to the projects dashboard and user profile
+merge_request:
+author:
diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml
deleted file mode 100644
index 0083798be08..00000000000
--- a/changelogs/unreleased/wip-mr-from-commits.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Mark MR as WIP when pushing WIP commits
-merge_request: 8124
-author: Jurre Stender @jurre
diff --git a/changelogs/unreleased/workhorse-1-4-0.yml b/changelogs/unreleased/workhorse-1-4-0.yml
new file mode 100644
index 00000000000..b55fabddb0f
--- /dev/null
+++ b/changelogs/unreleased/workhorse-1-4-0.yml
@@ -0,0 +1,4 @@
+---
+title: Use gitlab-workhorse 1.4.0
+merge_request: 9724
+author:
diff --git a/changelogs/unreleased/zj-builds-to-jobs-api.yml b/changelogs/unreleased/zj-builds-to-jobs-api.yml
new file mode 100644
index 00000000000..473dd9bc8ed
--- /dev/null
+++ b/changelogs/unreleased/zj-builds-to-jobs-api.yml
@@ -0,0 +1,4 @@
+---
+title: Rename builds to job for the v4 API
+merge_request: 9463
+author:
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
deleted file mode 100644
index 2494884f5c9..00000000000
--- a/changelogs/unreleased/zj-format-chat-messages.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reformat messages ChatOps
-merge_request: 8528
-author:
diff --git a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml b/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
deleted file mode 100644
index 044f4ae627d..00000000000
--- a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated GitlabCiService
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml
deleted file mode 100644
index 464c5948f8c..00000000000
--- a/changelogs/unreleased/zj-requeue-pending-delete.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Requeue pending deletion projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-variables-build-job.yml b/changelogs/unreleased/zj-variables-build-job.yml
new file mode 100644
index 00000000000..1cb0919f824
--- /dev/null
+++ b/changelogs/unreleased/zj-variables-build-job.yml
@@ -0,0 +1,4 @@
+---
+title: Rename job environment variables to new terminology
+merge_request: 9756
+author:
diff --git a/config/application.rb b/config/application.rb
index 9088d3c432b..f9f01b66473 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -7,6 +7,7 @@ Bundler.require(:default, Rails.env)
module Gitlab
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis')
+ require_dependency Rails.root.join('lib/gitlab/request_context')
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
@@ -25,7 +26,8 @@ module Gitlab
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
- #{config.root}/app/workers/concerns))
+ #{config.root}/app/workers/concerns
+ #{config.root}/app/services/concerns))
config.generators.templates.push("#{config.root}/generator_templates")
@@ -90,6 +92,7 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
+ # Support legacy unicode file named img emojis, `1F939.png`
config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
@@ -100,9 +103,6 @@ module Gitlab
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "lib/ace.js"
- config.assets.precompile << "lib/cropper.js"
- config.assets.precompile << "lib/raphael.js"
- config.assets.precompile << "u2f.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
@@ -120,7 +120,7 @@ module Gitlab
credentials: true,
headers: :any,
methods: :any,
- expose: ['Link']
+ expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page']
end
# Cross-origin requests must not have the session cookie available
@@ -130,7 +130,7 @@ module Gitlab
credentials: false,
headers: :any,
methods: :any,
- expose: ['Link']
+ expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page']
end
end
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 7336d7c842a..072ed8a3864 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -320,3 +320,9 @@
:why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE
:versions: []
:when: 2017-02-08 22:35:00.225232000 Z
+- - :approve
+ - opener
+ - :who: Mike Greiling
+ :why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt
+ :versions: []
+ :when: 2017-02-21 22:33:41.729629000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a82ff605a70..ba7f6773985 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -89,7 +89,7 @@ production: &base
issues: true
merge_requests: true
wiki: true
- snippets: false
+ snippets: true
builds: true
container_registry: true
@@ -157,8 +157,8 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
- # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
- # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+ # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
+ # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
## Mattermost
## For enabling Add to Mattermost button
@@ -177,9 +177,9 @@ production: &base
# Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc.
# Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
cron_jobs:
- # Flag stuck CI builds as failed
- stuck_ci_builds_worker:
- cron: "0 0 * * *"
+ # Flag stuck CI jobs as failed
+ stuck_ci_jobs_worker:
+ cron: "0 * * * *"
# Remove expired build artifacts
expire_build_artifacts_worker:
cron: "50 * * * *"
@@ -441,19 +441,21 @@ production: &base
shared:
# path: /mnt/gitlab # Default: shared
+ # Gitaly settings
+ gitaly:
+ # The socket_path setting is optional and obsolete. When this is set
+ # GitLab assumes it can reach a Gitaly services via a Unix socket at
+ # this path. When this is commented out GitLab will not use Gitaly.
+ #
+ # This setting is obsolete because we expect it to be moved under
+ # repositories/storages in GitLab 9.1.
+ #
+ # socket_path: tmp/sockets/gitaly.socket
#
# 4. Advanced settings
# ==========================
- # GitLab Satellites
- #
- # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
- # least. This setting is fed to 'rm -rf' in
- # db/migrate/20151023144219_remove_satellites.rb
- satellites:
- path: /home/git/gitlab-satellites/
-
## Repositories settings
repositories:
# Paths where repositories can be stored. Give the canonicalized absolute pathname.
@@ -461,7 +463,8 @@ production: &base
# gitlab-shell invokes Dir.pwd inside the repository path and that results
# real path not the symlink.
storages: # You must have at least a `default` storage path.
- default: /home/git/repositories/
+ default:
+ path: /home/git/repositories/
## Backup settings
backup:
@@ -483,6 +486,8 @@ production: &base
# multipart_chunk_size: 104857600
# # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# # encryption: 'AES256'
+ # # Specifies Amazon S3 storage class to use for backups, this is optional
+ # # storage_class: 'STANDARD'
## GitLab Shell settings
gitlab_shell:
@@ -568,11 +573,10 @@ test:
# In order to setup it correctly you need to specify
# your system username you use to run GitLab
# user: YOUR_USERNAME
- satellites:
- path: tmp/tests/gitlab-satellites/
repositories:
storages:
- default: tmp/tests/repositories/
+ default:
+ path: tmp/tests/repositories/
backup:
path: tmp/tests/backups
gitlab_shell:
@@ -586,7 +590,7 @@ test:
new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new"
jira:
title: "JIRA"
- url: https://sample_company.atlasian.net
+ url: https://sample_company.atlassian.net
project_key: PROJECT
ldap:
enabled: false
diff --git a/config/initializers/inflections.rb b/config/initializers/0_inflections.rb
index d4197da3fa9..d4197da3fa9 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/0_inflections.rb
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 3f716dd8833..62020fa9a75 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -14,12 +14,15 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if on_standard_port?(gitlab)
- custom_port = nil
- else
- custom_port = ":#{gitlab.port}"
- end
- [ gitlab.protocol,
+ custom_port =
+ if on_standard_port?(gitlab)
+ nil
+ else
+ ":#{gitlab.port}"
+ end
+
+ [
+ gitlab.protocol,
"://",
gitlab.host,
custom_port,
@@ -80,7 +83,9 @@ class Settings < Settingslogic
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
- [ config.protocol,
+
+ [
+ config.protocol,
"://",
config.host,
custom_port
@@ -160,15 +165,16 @@ if github_settings
github_settings["args"] ||= Settingslogic.new({})
- if github_settings["url"].include?(github_default_url)
- github_settings["args"]["client_options"] = OmniAuth::Strategies::GitHub.default_options[:client_options]
- else
- github_settings["args"]["client_options"] = {
- "site" => File.join(github_settings["url"], "api/v3"),
- "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
- "token_url" => File.join(github_settings["url"], "login/oauth/access_token")
- }
- end
+ github_settings["args"]["client_options"] =
+ if github_settings["url"].include?(github_default_url)
+ OmniAuth::Strategies::GitHub.default_options[:client_options]
+ else
+ {
+ "site" => File.join(github_settings["url"], "api/v3"),
+ "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
+ "token_url" => File.join(github_settings["url"], "login/oauth/access_token")
+ }
+ end
end
Settings['shared'] ||= Settingslogic.new({})
@@ -180,7 +186,7 @@ Settings['issues_tracker'] ||= {}
# GitLab
#
Settings['gitlab'] ||= Settingslogic.new({})
-Settings.gitlab['default_projects_limit'] ||= 10
+Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
@@ -215,7 +221,7 @@ Settings.gitlab['session_expire_delay'] ||= 10080
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
-Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
+Settings.gitlab.default_projects_features['snippets'] = true if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
@@ -272,8 +278,8 @@ Settings.pages['host'] ||= "example.com"
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
Settings.pages['url'] ||= Settings.send(:build_pages_url)
-Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
-Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
+Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
+Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
#
# Git LFS
@@ -302,9 +308,9 @@ Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar[
# Cron Jobs
#
Settings['cron_jobs'] ||= Settingslogic.new({})
-Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
-Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
+Settings.cron_jobs['stuck_ci_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
@@ -360,8 +366,13 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s
#
Settings['repositories'] ||= Settingslogic.new({})
Settings.repositories['storages'] ||= {}
-# Setting gitlab_shell.repos_path is DEPRECATED and WILL BE REMOVED in version 9.0
-Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/'
+unless Settings.repositories.storages['default']
+ Settings.repositories.storages['default'] ||= {}
+ # We set the path only if the default storage doesn't exist, in case it exists
+ # but follows the pre-9.0 configuration structure. `6_validations.rb` initializer
+ # will validate all storages and throw a relevant error to the user if necessary.
+ Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/'
+end
#
# The repository_downloads_path is used to remove outdated repository
@@ -370,11 +381,11 @@ Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'
# data-integrity issue. In this case, we sets it to the default
# repository_downloads_path value.
#
-repositories_storages_path = Settings.repositories.storages.values
+repositories_storages = Settings.repositories.storages.values
repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
-if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) }
+if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) }
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
end
@@ -393,6 +404,7 @@ if Settings.backup['upload']['connection']
end
Settings.backup['upload']['multipart_chunk_size'] ||= 104857600
Settings.backup['upload']['encryption'] ||= nil
+Settings.backup['upload']['storage_class'] ||= nil
#
# Git
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92f64e1647..9e24f42d284 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -4,8 +4,8 @@ end
def find_parent_path(name, path)
parent = Pathname.new(path).realpath.parent
- Gitlab.config.repositories.storages.detect do |n, p|
- name != n && Pathname.new(p).realpath == parent
+ Gitlab.config.repositories.storages.detect do |n, rs|
+ name != n && Pathname.new(rs['path']).realpath == parent
end
end
@@ -13,17 +13,33 @@ def storage_validation_error(message)
raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
end
-def validate_storages
+def validate_storages_config
storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
- Gitlab.config.repositories.storages.each do |name, path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- parent_name, _parent_path = find_parent_path(name, path)
+ if repository_storage.is_a?(String)
+ raise "#{name} is not a valid storage, because it has no `path` key. " \
+ "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
+ "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
+ "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
+ end
+
+ if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
+ storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
+ end
+ end
+end
+
+def validate_storages_paths
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ parent_name, _parent_path = find_parent_path(name, repository_storage['path'])
if parent_name
storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
end
end
end
-validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
+validate_storages_config
+validate_storages_paths unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
new file mode 100644
index 00000000000..07dd30f0a24
--- /dev/null
+++ b/config/initializers/8_gitaly.rb
@@ -0,0 +1,2 @@
+# Make sure we initialize a Gitaly channel before Sidekiq starts multi-threaded execution.
+Gitlab::GitalyClient.channel unless Rails.env.test?
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
new file mode 100644
index 00000000000..5e0eefdb154
--- /dev/null
+++ b/config/initializers/8_metrics.rb
@@ -0,0 +1,192 @@
+# Autoload all classes that we want to instrument, and instrument the methods we
+# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
+# that we can stub it for testing, as it is only called when metrics are
+# enabled.
+#
+# rubocop:disable Metrics/AbcSize
+def instrument_classes(instrumentation)
+ instrumentation.instrument_instance_methods(Gitlab::Shell)
+
+ instrumentation.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ next unless const.is_a?(Module)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Path to search => prefix to strip from constant
+ paths_to_instrument = {
+ %w(app finders) => %w(app finders),
+ %w(app mailers emails) => %w(app mailers),
+ # Don't instrument `app/services/concerns`
+ # It contains modules that are included in the services.
+ # The services themselves are instrumented so the methods from the modules
+ # are included.
+ %w(app services [^concerns]**) => %w(app services),
+ %w(lib gitlab conflicts) => ['lib'],
+ %w(lib gitlab diff) => ['lib'],
+ %w(lib gitlab email message) => ['lib'],
+ %w(lib gitlab checks) => ['lib']
+ }
+
+ paths_to_instrument.each do |(path, prefix)|
+ prefix = Rails.root.join(*prefix)
+
+ Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
+ path = Pathname.new(file_path).relative_path_from(prefix)
+ const = path.to_s.sub('.rb', '').camelize.constantize
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
+ instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Banzai::Renderer)
+ instrumentation.instrument_methods(Banzai::Querying)
+
+ instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
+ instrumentation.instrument_instance_methods(Banzai::Redactor)
+ instrumentation.instrument_methods(Banzai::NoteRenderer)
+
+ [Issuable, Mentionable, Participable].each do |klass|
+ instrumentation.instrument_instance_methods(klass)
+ instrumentation.instrument_instance_methods(klass::ClassMethods)
+ end
+
+ instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
+ instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
+
+ # Instrument the classes used for checking if somebody has push access.
+ instrumentation.instrument_instance_methods(Gitlab::GitAccess)
+ instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ instrumentation.instrument_instance_methods(API::Helpers)
+
+ instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ instrumentation.instrument_methods(namespace_mod)
+ instrumentation.instrument_methods(namespace_mod::Document)
+ end
+
+ instrumentation.instrument_methods(Rinku)
+ instrumentation.instrument_instance_methods(Repository)
+
+ instrumentation.instrument_methods(Gitlab::Highlight)
+ instrumentation.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ instrumentation.instrument_method(Project, :visible_to_user)
+end
+# rubocop:enable Metrics/AbcSize
+
+if Gitlab::Metrics.enabled?
+ require 'pathname'
+ require 'influxdb'
+ require 'connection_pool'
+ require 'method_source'
+
+ # These are manually require'd so the classes are registered properly with
+ # ActiveSupport.
+ require_dependency 'gitlab/metrics/subscribers/action_view'
+ require_dependency 'gitlab/metrics/subscribers/active_record'
+ require_dependency 'gitlab/metrics/subscribers/rails_cache'
+
+ Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::Metrics::RackMiddleware)
+ config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
+ end
+
+ Sidekiq.configure_server do |config|
+ config.server_middleware do |chain|
+ chain.add Gitlab::Metrics::SidekiqMiddleware
+ end
+ end
+
+ # This instruments all methods residing in app/models that (appear to) use any
+ # of the ActiveRecord methods. This has to take place _after_ initializing as
+ # for some unknown reason calling eager_load! earlier breaks Devise.
+ Gitlab::Application.config.after_initialize do
+ Rails.application.eager_load!
+
+ models = Rails.root.join('app', 'models').to_s
+
+ regex = Regexp.union(
+ ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
+ )
+
+ Gitlab::Metrics::Instrumentation.
+ instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
+ # Instrumenting the ApplicationSetting class can lead to an infinite
+ # loop. Since the data is cached any way we don't really need to
+ # instrument it.
+ if klass == ApplicationSetting
+ false
+ else
+ loc = method.source_location
+
+ loc && loc[0].start_with?(models) && method.source =~ regex
+ end
+ end
+ end
+
+ Gitlab::Metrics::Instrumentation.configure do |config|
+ instrument_classes(config)
+ end
+
+ GC::Profiler.enable
+
+ Gitlab::Metrics::Sampler.new.start
+
+ module TrackNewRedisConnections
+ def connect(*args)
+ val = super
+
+ if current_transaction = Gitlab::Metrics::Transaction.current
+ current_transaction.increment(:new_redis_connections, 1)
+ end
+
+ val
+ end
+ end
+
+ class ::Redis::Client
+ prepend TrackNewRedisConnections
+ end
+end
diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/acts_as_taggable.rb
new file mode 100644
index 00000000000..c564c0cab11
--- /dev/null
+++ b/config/initializers/acts_as_taggable.rb
@@ -0,0 +1,5 @@
+ActsAsTaggableOn.strict_case_match = true
+
+# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed
+# since the count is not used anywhere its better performance wise to disable this cache
+ActsAsTaggableOn.tags_counter = false
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 738dbeefc11..3b1317030bc 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -24,7 +24,7 @@ Devise.setup do |config|
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
- config.authentication_keys = [ :login ]
+ config.authentication_keys = [:login]
# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
@@ -36,12 +36,12 @@ Devise.setup do |config|
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
- config.case_insensitive_keys = [ :email ]
+ config.case_insensitive_keys = [:email]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
# modifying a user and when used to authenticate or find a user. Default is :email.
- config.strip_whitespace_keys = [ :email ]
+ config.strip_whitespace_keys = [:email]
# Tell if authentication through request.params is enabled. True by default.
# config.params_authenticatable = true
@@ -124,7 +124,7 @@ Devise.setup do |config|
config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
- config.unlock_keys = [ :email ]
+ config.unlock_keys = [:email]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 88cd0f5f652..a5636765774 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -6,9 +6,14 @@ Doorkeeper.configure do
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
# Put your resource owner authentication logic here.
- # Ensure user is redirected to redirect_uri after login
- session[:user_return_to] = request.fullpath
- current_user || redirect_to(new_user_session_url)
+ if current_user
+ current_user
+ else
+ # Ensure user is redirected to redirect_uri after login
+ session[:user_return_to] = request.fullpath
+ redirect_to(new_user_session_url)
+ nil
+ end
end
resource_owner_from_credentials do |routes|
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
new file mode 100644
index 00000000000..700ca25b884
--- /dev/null
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -0,0 +1,36 @@
+Doorkeeper::OpenidConnect.configure do
+ issuer Gitlab.config.gitlab.url
+
+ jws_private_key Rails.application.secrets.jws_private_key
+
+ resource_owner_from_access_token do |access_token|
+ User.active.find_by(id: access_token.resource_owner_id)
+ end
+
+ auth_time_from_resource_owner do |user|
+ user.current_sign_in_at
+ end
+
+ reauthenticate_resource_owner do |user, return_to|
+ store_location_for user, return_to
+ sign_out user
+ redirect_to new_user_session_url
+ end
+
+ subject do |user|
+ # hash the user's ID with the Rails secret_key_base to avoid revealing it
+ Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
+ end
+
+ claims do
+ with_options scope: :openid do |o|
+ o.claim(:name) { |user| user.name }
+ o.claim(:nickname) { |user| user.username }
+ o.claim(:email) { |user| user.public_email }
+ o.claim(:email_verified) { |user| true if user.public_email? }
+ o.claim(:website) { |user| user.full_website_url if user.website_url? }
+ o.claim(:profile) { |user| Rails.application.routes.url_helpers.user_url user }
+ o.claim(:picture) { |user| user.avatar_url }
+ end
+ end
+end
diff --git a/config/initializers/etag_caching.rb b/config/initializers/etag_caching.rb
new file mode 100644
index 00000000000..eba88801141
--- /dev/null
+++ b/config/initializers/etag_caching.rb
@@ -0,0 +1,4 @@
+# This middleware has to come after Gitlab::Metrics::RackMiddleware
+# in the middleware stack, because it tracks events with
+# GitLab Performance Monitoring
+Rails.application.config.middleware.use(Gitlab::EtagCaching::Middleware)
diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb
new file mode 100644
index 00000000000..cb37f9ed22c
--- /dev/null
+++ b/config/initializers/fix_local_cache_middleware.rb
@@ -0,0 +1,24 @@
+module LocalCacheRegistryCleanupWithEnsure
+ LocalCacheRegistry =
+ ActiveSupport::Cache::Strategy::LocalCache::LocalCacheRegistry
+ LocalStore =
+ ActiveSupport::Cache::Strategy::LocalCache::LocalStore
+
+ def call(env)
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ end
+ cleanup_after_response = true # ADDED THIS LINE
+ response
+ rescue Rack::Utils::InvalidParameterError
+ [400, {}, []]
+ ensure # ADDED ensure CLAUSE to cleanup when something is thrown
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
+ cleanup_after_response
+ end
+end
+
+ActiveSupport::Cache::Strategy::LocalCache::Middleware
+ .prepend(LocalCacheRegistryCleanupWithEnsure)
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 703f24f93b2..1ebe3c7a742 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -1,5 +1,5 @@
module Gollum
- GIT_ADAPTER = "rugged"
+ GIT_ADAPTER = "rugged".freeze
end
require "gollum-lib"
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 4c91a61fb4a..959daa93f78 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -1,4 +1,4 @@
HealthCheck.setup do |config|
- config.standard_checks = ['database', 'migrations', 'cache']
- config.full_checks = ['database', 'migrations', 'cache']
+ config.standard_checks = %w(database migrations cache)
+ config.full_checks = %w(database migrations cache)
end
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
deleted file mode 100644
index e0702e06cc9..00000000000
--- a/config/initializers/metrics.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-# Autoload all classes that we want to instrument, and instrument the methods we
-# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
-# that we can stub it for testing, as it is only called when metrics are
-# enabled.
-#
-# rubocop:disable Metrics/AbcSize
-def instrument_classes(instrumentation)
- instrumentation.instrument_instance_methods(Gitlab::Shell)
-
- instrumentation.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name)
-
- next unless const.is_a?(Module)
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- ['app', 'finders'] => ['app', 'finders'],
- ['app', 'mailers', 'emails'] => ['app', 'mailers'],
- ['app', 'services', '**'] => ['app', 'services'],
- ['lib', 'gitlab', 'conflicts'] => ['lib'],
- ['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib'],
- ['lib', 'gitlab', 'checks'] => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
- end
-
- instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
- instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- [
- :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
- :Tag, :TagCollection, :Tree
- ].each do |name|
- const = Rugged.const_get(name)
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
-
- # Instruments all Banzai filters and reference parsers
- {
- Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
- ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
- }.each do |const_name, path|
- Dir[path].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai.const_get(const_name).const_get(klass)
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
- end
-
- instrumentation.instrument_methods(Banzai::Renderer)
- instrumentation.instrument_methods(Banzai::Querying)
-
- instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
- instrumentation.instrument_instance_methods(Banzai::Redactor)
- instrumentation.instrument_methods(Banzai::NoteRenderer)
-
- [Issuable, Mentionable, Participable].each do |klass|
- instrumentation.instrument_instance_methods(klass)
- instrumentation.instrument_instance_methods(klass::ClassMethods)
- end
-
- instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
- instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- instrumentation.instrument_instance_methods(Gitlab::GitAccess)
- instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- instrumentation.instrument_instance_methods(API::Helpers)
-
- instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
- instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
-
- [:XML, :HTML].each do |namespace|
- namespace_mod = Nokogiri.const_get(namespace)
-
- instrumentation.instrument_methods(namespace_mod)
- instrumentation.instrument_methods(namespace_mod::Document)
- end
-
- instrumentation.instrument_methods(Rinku)
- instrumentation.instrument_instance_methods(Repository)
-
- instrumentation.instrument_methods(Gitlab::Highlight)
- instrumentation.instrument_instance_methods(Gitlab::Highlight)
-
- # This is a Rails scope so we have to instrument it manually.
- instrumentation.instrument_method(Project, :visible_to_user)
-end
-# rubocop:enable Metrics/AbcSize
-
-if Gitlab::Metrics.enabled?
- require 'pathname'
- require 'influxdb'
- require 'connection_pool'
- require 'method_source'
-
- # These are manually require'd so the classes are registered properly with
- # ActiveSupport.
- require 'gitlab/metrics/subscribers/action_view'
- require 'gitlab/metrics/subscribers/active_record'
- require 'gitlab/metrics/subscribers/rails_cache'
-
- Gitlab::Application.configure do |config|
- config.middleware.use(Gitlab::Metrics::RackMiddleware)
- config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
- end
-
- Sidekiq.configure_server do |config|
- config.server_middleware do |chain|
- chain.add Gitlab::Metrics::SidekiqMiddleware
- end
- end
-
- # This instruments all methods residing in app/models that (appear to) use any
- # of the ActiveRecord methods. This has to take place _after_ initializing as
- # for some unknown reason calling eager_load! earlier breaks Devise.
- Gitlab::Application.config.after_initialize do
- Rails.application.eager_load!
-
- models = Rails.root.join('app', 'models').to_s
-
- regex = Regexp.union(
- ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
- )
-
- Gitlab::Metrics::Instrumentation.
- instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
- # Instrumenting the ApplicationSetting class can lead to an infinite
- # loop. Since the data is cached any way we don't really need to
- # instrument it.
- if klass == ApplicationSetting
- false
- else
- loc = method.source_location
-
- loc && loc[0].start_with?(models) && method.source =~ regex
- end
- end
- end
-
- Gitlab::Metrics::Instrumentation.configure do |config|
- instrument_classes(config)
- end
-
- GC::Profiler.enable
-
- Gitlab::Metrics::Sampler.new.start
-
- module TrackNewRedisConnections
- def connect(*args)
- val = super
-
- if current_transaction = Gitlab::Metrics::Transaction.current
- current_transaction.increment(:new_redis_connections, 1)
- end
-
- val
- end
- end
-
- class ::Redis::Client
- prepend TrackNewRedisConnections
- end
-end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index ab5a0561b8c..f7fa6d1c2de 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -20,15 +20,12 @@ OmniAuth.config.before_request_phase do |env|
end
if Gitlab.config.omniauth.enabled
- Gitlab.config.omniauth.providers.each do |provider|
- if provider['name'] == 'kerberos'
- require 'omniauth-kerberos'
- end
- end
+ provider_names = Gitlab.config.omniauth.providers.map(&:name)
+ require 'omniauth-kerberos' if provider_names.include?('kerberos')
end
module OmniAuth
module Strategies
- autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+ autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
end
end
diff --git a/config/initializers/request_context.rb b/config/initializers/request_context.rb
new file mode 100644
index 00000000000..0b485fc1adc
--- /dev/null
+++ b/config/initializers/request_context.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do |config|
+ config.middleware.insert_after RequestStore::Middleware, Gitlab::RequestContext
+end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 0ef9f51e5cf..70177995356 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -1,22 +1,41 @@
-module RspecProfilingConnection
- def establish_connection
- ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+module RspecProfilingExt
+ module PSQL
+ def establish_connection
+ ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+ end
end
-end
-module RspecProfilingGitBranchCi
- def branch
- ENV['CI_BUILD_REF_NAME'] || super
+ module Git
+ def branch
+ ENV['CI_COMMIT_REF_NAME'] || super
+ end
+ end
+
+ module Run
+ def example_finished(*args)
+ super
+ rescue => err
+ return if @already_logged_example_finished_error
+
+ $stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed."
+ @already_logged_example_finished_error = true
+ end
+
+ alias_method :example_passed, :example_finished
+ alias_method :example_failed, :example_finished
end
end
if Rails.env.test?
RspecProfiling.configure do |config|
if ENV['RSPEC_PROFILING_POSTGRES_URL']
- RspecProfiling::Collectors::PSQL.prepend(RspecProfilingConnection)
+ RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
config.collector = RspecProfiling::Collectors::PSQL
end
end
- RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI')
+ if ENV.has_key?('CI')
+ RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
+ RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+ end
end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 291fa6c0abc..f9c1d2165d3 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -24,7 +24,8 @@ def create_tokens
defaults = {
secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
- db_key_base: generate_new_secure_token
+ db_key_base: generate_new_secure_token,
+ jws_private_key: generate_new_rsa_private_key
}
missing_secrets = set_missing_keys(defaults)
@@ -41,6 +42,10 @@ def generate_new_secure_token
SecureRandom.hex(64)
end
+def generate_new_rsa_private_key
+ OpenSSL::PKey::RSA.new(2048).to_pem
+end
+
def warn_missing_secret(secret)
warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0c4516b70f0..ecd73956488 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -19,6 +19,12 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
+ config.on :startup do
+ # Clear any connections that might have been obtained before starting
+ # Sidekiq (e.g. in an initializer).
+ ActiveRecord::Base.clear_all_connections!
+ end
+
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
@@ -36,7 +42,7 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
- config = ActiveRecord::Base.configurations[Rails.env] ||
+ config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index cd869657c53..fc4f02453d7 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -21,4 +21,4 @@ gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |pro
end.compact
Rails.application.config.action_dispatch.trusted_proxies = (
- [ '127.0.0.1', '::1' ] + gitlab_trusted_proxies)
+ ['127.0.0.1', '::1'] + gitlab_trusted_proxies)
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
new file mode 100644
index 00000000000..3d83fb92d56
--- /dev/null
+++ b/config/initializers/warden.rb
@@ -0,0 +1,5 @@
+Rails.application.configure do |config|
+ Warden::Manager.after_set_user do |user, auth, opts|
+ Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
+ end
+end
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
index 84d809741c4..064e5964f09 100644
--- a/config/initializers/workhorse_multipart.rb
+++ b/config/initializers/workhorse_multipart.rb
@@ -10,7 +10,7 @@ end
#
module Gitlab
module StrongParameterScalars
- GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
+ GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile].freeze
def permitted_scalar?(value)
super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
diff --git a/config/karma.config.js b/config/karma.config.js
index 2f3cc932413..eb082dd28bf 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,22 +1,23 @@
var path = require('path');
+var webpack = require('webpack');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
-// add coverage instrumentation to babel config
-if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) {
- var babelConfig = webpackConfig.module.rules.find(function (rule) {
- return rule.loader === 'babel-loader';
+// remove problematic plugins
+if (webpackConfig.plugins) {
+ webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
+ return !(
+ plugin instanceof webpack.optimize.CommonsChunkPlugin ||
+ plugin instanceof webpack.DefinePlugin
+ );
});
-
- babelConfig.options = babelConfig.options || {};
- babelConfig.options.plugins = babelConfig.options.plugins || [];
- babelConfig.options.plugins.push('istanbul');
}
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
- config.set({
+
+ var karmaConfig = {
basePath: ROOT_PATH,
browsers: ['PhantomJS'],
frameworks: ['jasmine'],
@@ -25,16 +26,22 @@ module.exports = function(config) {
{ pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
],
preprocessors: {
- 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+ 'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
},
- reporters: [progressReporter, 'coverage-istanbul'],
- coverageIstanbulReporter: {
+ reporters: [progressReporter],
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ };
+
+ if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
+ karmaConfig.reporters.push('coverage-istanbul');
+ karmaConfig.coverageIstanbulReporter = {
reports: ['html', 'text-summary'],
dir: 'coverage-javascript/',
subdir: '.',
fixWebpackSourcePaths: true
- },
- webpack: webpackConfig,
- webpackMiddleware: { stats: 'errors-only' },
- });
+ };
+ }
+
+ config.set(karmaConfig);
};
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 1d728282d90..14d49885fb3 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -60,6 +60,7 @@ en:
scopes:
api: Access your API
read_user: Read user information
+ openid: Authenticate using OpenID Connect
flash:
applications:
diff --git a/config/newrelic.yml b/config/newrelic.yml
deleted file mode 100644
index 9ef922a38d9..00000000000
--- a/config/newrelic.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-# New Relic configuration file
-#
-# This file is here to make sure the New Relic gem stays
-# quiet by default.
-#
-# To enable and configure New Relic, please use
-# environment variables, e.g. NEW_RELIC_ENABLED=true
-
-production:
- enabled: false
-
-development:
- enabled: false
-
-test:
- enabled: false
diff --git a/config/routes.rb b/config/routes.rb
index 06d565df469..1a851da6203 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,14 +22,13 @@ Rails.application.routes.draw do
authorizations: 'oauth/authorizations'
end
+ use_doorkeeper_openid_connect
+
# Autocomplete
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects'
- # Emojis
- resources :emojis, only: :index
-
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index b09c05826a7..fcbe2e2c435 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -2,6 +2,11 @@ namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
+ resources :impersonation_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
member do
get :projects
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index 47a049d5b20..8d23aa8fbf6 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -5,11 +5,5 @@ namespace :ci do
resource :lint, only: [:show, :create]
- resources :projects, only: [:index, :show] do
- member do
- get :status, to: 'projects#badge'
- end
- end
-
- root to: 'projects#index'
+ root to: redirect('/')
end
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index adc3ad207cc..8e380a0b0ac 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
+ patch :bulk_restore
end
member do
patch :restore
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 6b91485da9e..07c341999ea 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -21,7 +21,7 @@ resource :profile, only: [:show, :update] do
end
end
resource :preferences, only: [:show, :update]
- resources :keys, only: [:index, :show, :new, :create, :destroy]
+ resources :keys, only: [:index, :show, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 84f123ff717..44b8ae7aedd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do
collection do
- get 'emojis'
get 'members'
get 'issues'
get 'merge_requests'
@@ -58,6 +57,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
member do
+ get :charts
get :commits
get :ci
get :languages
@@ -100,7 +100,7 @@ constraints(ProjectUrlConstrainer.new) do
get :merge_check
post :merge
get :merge_widget_refresh
- post :cancel_merge_when_build_succeeds
+ post :cancel_merge_when_pipeline_succeeds
get :ci_status
get :ci_environments_status
post :toggle_subscription
@@ -135,11 +135,16 @@ constraints(ProjectUrlConstrainer.new) do
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
- resources :triggers, only: [:index, :create, :destroy]
+ resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
+ member do
+ post :take_ownership
+ end
+ end
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
+ get :charts
end
member do
@@ -154,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do
member do
post :stop
get :terminal
+ get :metrics
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end
@@ -265,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
- resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+ resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
post :resolve
@@ -273,6 +279,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
+
resources :boards, only: [:index, :show] do
scope module: :boards do
resources :issues, only: [:index, :update]
@@ -321,6 +329,7 @@ constraints(ProjectUrlConstrainer.new) do
resource :members, only: [:show]
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
+ resource :repository, only: [:show], controller: :repository
end
# Since both wiki and repository routing contains wildcard characters
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index dad746d59a1..a6b3f5d4693 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -1,4 +1,4 @@
-WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 97620cc9c7f..9d2066a6490 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
+ - [upload_checksum, 1]
- [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
@@ -51,3 +52,4 @@
- [cronjob, 1]
- [default, 1]
- [pages, 1]
+ - [system_hook_push, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 15899993874..c6794d6b944 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -5,17 +5,22 @@ var path = require('path');
var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
+var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
- application: './application.js',
+ common: './commons/index.js',
+ common_vue: ['vue', 'vue-resource'],
+ common_d3: ['d3'],
+ main: './main.js',
blob_edit: './blob_edit/blob_edit_bundle.js',
boards: './boards/boards_bundle.js',
simulate_drag: './test_utils/simulate_drag.js',
@@ -26,25 +31,25 @@ var config = {
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js',
+ groups_list: './groups_list.js',
issuable: './issuable/issuable_bundle.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js',
+ monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
terminal: './terminal/terminal_bundle.js',
+ u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
- lib_chart: './lib/chart.js',
- lib_d3: './lib/d3.js',
- lib_vue: './lib/vue_resource.js',
vue_pipelines: './vue_pipelines_index/index.js',
},
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
- filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js'
+ filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
},
devtool: 'inline-source-map',
@@ -52,15 +57,13 @@ var config = {
module: {
rules: [
{
- test: /\.(js|es6)$/,
+ test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
- loader: 'babel-loader',
- options: {
- presets: [
- ["es2015", {"modules": false}],
- 'stage-2'
- ]
- }
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.svg$/,
+ use: 'raw-loader'
}
]
},
@@ -75,17 +78,61 @@ var config = {
modules: false,
assets: true
}),
+
+ // prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/),
+
+ // fix legacy jQuery plugins which depend on globals
+ new webpack.ProvidePlugin({
+ $: 'jquery',
+ jQuery: 'jquery',
+ }),
+
+ // use deterministic module ids in all environments
+ IS_PRODUCTION ?
+ new webpack.HashedModuleIdsPlugin() :
+ new webpack.NamedModulesPlugin(),
+
+ // create cacheable common library bundle for all vue chunks
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'common_vue',
+ chunks: [
+ 'boards',
+ 'commit_pipelines',
+ 'cycle_analytics',
+ 'diff_notes',
+ 'environments',
+ 'environments_folder',
+ 'issuable',
+ 'merge_conflicts',
+ 'vue_pipelines',
+ ],
+ minChunks: function(module, count) {
+ return module.resource && (/vue_shared/).test(module.resource);
+ },
+ }),
+
+ // create cacheable common library bundle for all d3 chunks
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'common_d3',
+ chunks: ['graphs', 'users', 'monitoring'],
+ }),
+
+ // create cacheable common library bundles
+ new webpack.optimize.CommonsChunkPlugin({
+ names: ['main', 'common', 'runtime'],
+ }),
],
resolve: {
- extensions: ['.js', '.es6', '.js.es6'],
+ extensions: ['.js'],
alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
- 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
+ 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
+ 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+ 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
- 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
+ 'vue$': 'vue/dist/vue.common.js',
}
}
}
@@ -120,4 +167,16 @@ if (IS_DEV_SERVER) {
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
}
+if (WEBPACK_REPORT) {
+ config.plugins.push(
+ new BundleAnalyzerPlugin({
+ analyzerMode: 'static',
+ generateStatsFile: true,
+ openAnalyzer: false,
+ reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
+ statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
+ })
+ );
+}
+
module.exports = config;
diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb
index 29b8081055d..bc2d74c8034 100644
--- a/db/fixtures/development/13_comments.rb
+++ b/db/fixtures/development/13_comments.rb
@@ -1,7 +1,7 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- Issue.all.each do |issue|
+ Issue.find_each do |issue|
project = issue.project
project.team.users.each do |user|
@@ -16,7 +16,7 @@ Gitlab::Seeder.quiet do
end
end
- MergeRequest.all.each do |mr|
+ MergeRequest.find_each do |mr|
project = mr.project
project.team.users.each do |user|
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
index ea343c26b69..137a036edaf 100644
--- a/db/fixtures/development/15_award_emoji.rb
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -1,7 +1,7 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- emoji = Gitlab::AwardEmoji.emojis.keys
+ emoji = Gitlab::Emoji.emojis.keys
Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
project = issue.project
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 747901dd634..4bc735916c1 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
- options = {
- committer: issue.project.repository.user_to_committer(@user),
- author: issue.project.repository.user_to_committer(@user),
- commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
-
- commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+ commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
-
GitPushService.new(issue.project,
@user,
oldrev: issue.project.repository.commit("master").sha,
diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/19_nested_groups.rb
new file mode 100644
index 00000000000..d8dddc3fee9
--- /dev/null
+++ b/db/fixtures/development/19_nested_groups.rb
@@ -0,0 +1,69 @@
+require './spec/support/sidekiq'
+
+def create_group_with_parents(user, full_path)
+ parent_path = nil
+ group = nil
+
+ until full_path.blank?
+ path, _, full_path = full_path.partition('/')
+
+ if parent_path
+ parent = Group.find_by_full_path(parent_path)
+
+ parent_path += '/'
+ parent_path += path
+
+ group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute
+ else
+ parent_path = path
+
+ group = Group.find_by_full_path(parent_path) ||
+ Groups::CreateService.new(user, path: path).execute
+ end
+ end
+
+ group
+end
+
+Sidekiq::Testing.inline! do
+ Gitlab::Seeder.quiet do
+ project_urls = [
+ 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
+ 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/intel.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
+ ]
+
+ user = User.admins.first
+
+ project_urls.each_with_index do |url, i|
+ full_path = url.sub('https://android.googlesource.com/', '')
+ full_path = full_path.sub(/\.git\z/, '')
+ full_path, _, project_path = full_path.rpartition('/')
+ group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
+
+ params = {
+ import_url: url,
+ namespace_id: group.id,
+ path: project_path,
+ name: project_path,
+ description: FFaker::Lorem.sentence,
+ visibility_level: Gitlab::VisibilityLevel.values.sample
+ }
+
+ project = Projects::CreateService.new(user, params).execute
+ project.send(:_run_after_commit_queue)
+
+ if project.valid?
+ print '.'
+ else
+ print 'F'
+ end
+ end
+ end
+end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index e8de7ccf3db..66203486d53 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -8,7 +8,7 @@ class MigrateRepoSize < ActiveRecord::Migration
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
- repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+ repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default['path']
path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
begin
diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb
index 760b766828e..cd8b505de9f 100644
--- a/db/migrate/20160610201627_migrate_users_notification_level.rb
+++ b/db/migrate/20160610201627_migrate_users_notification_level.rb
@@ -1,7 +1,11 @@
class MigrateUsersNotificationLevel < ActiveRecord::Migration
+ DOWNTIME = false
+
# Migrates only users who changed their default notification level :participating
# creating a new record on notification settings table
+ DOWNTIME = false
+
def up
execute(%Q{
INSERT INTO notification_settings
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
index 63f7392e54f..7a8ed99c68f 100644
--- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -1,9 +1,15 @@
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :members, :requested_at
end
+
+ def down
+ remove_index :members, :requested_at if index_exists? :members, :requested_at
+ end
end
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
index dfa5110dea4..6ca486c63d1 100644
--- a/db/migrate/20160620115026_add_index_on_runners_locked.rb
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -4,9 +4,15 @@
class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_runners, :locked
end
+
+ def down
+ remove_index :ci_runners, :locked if index_exists? :ci_runners, :locked
+ end
end
diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
index 7c991c6d998..a05a4c679e3 100644
--- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
+++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
@@ -1,9 +1,15 @@
class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_commits, :user_id
end
+
+ def down
+ remove_index :ci_commits, :user_id if index_exists? :ci_commits, :user_id
+ end
end
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
index a853de3abfb..3f074723b4a 100644
--- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -5,8 +5,15 @@ class AddDeletedAtToNamespaces < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :namespaces, :deleted_at, :datetime
+
add_concurrent_index :namespaces, :deleted_at
end
+
+ def down
+ remove_index :namespaces, :deleted_at if index_exists? :namespaces, :deleted_at
+
+ remove_column :namespaces, :deleted_at
+ end
end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
index 10ef42afce1..6c5d7268e72 100644
--- a/db/migrate/20160808085602_add_index_for_build_token.rb
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -6,7 +6,11 @@ class AddIndexForBuildToken < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_builds, :token, unique: true
end
+
+ def down
+ remove_index :ci_builds, :token, unique: true if index_exists? :ci_builds, :token, unique: true
+ end
end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
index b6e8bb18e7b..8f693e97a58 100644
--- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -8,7 +8,11 @@ class AddIndexToNoteDiscussionId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :notes, :discussion_id
end
+
+ def down
+ remove_index :notes, :discussion_id if index_exists? :notes, :discussion_id
+ end
end
diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
index f2cf956adc9..bcad3416d04 100644
--- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -9,8 +9,15 @@ class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :users, :incoming_email_token, :string
+
add_concurrent_index :users, :incoming_email_token
end
+
+ def down
+ remove_index :users, :incoming_email_token if index_exists? :users, :incoming_email_token
+
+ remove_column :users, :incoming_email_token
+ end
end
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
index 8753e55e058..9cb44dfa9f9 100644
--- a/db/migrate/20160829114652_add_markdown_cache_columns.rb
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -26,7 +26,7 @@ class AddMarkdownCacheColumns < ActiveRecord::Migration
projects: [:description],
releases: [:description],
snippets: [:title, :content],
- }
+ }.freeze
def change
COLUMNS.each do |table, columns|
diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb
index 93f9821bc76..79a5fb29d64 100644
--- a/db/migrate/20160831214543_migrate_project_features.rb
+++ b/db/migrate/20160831214543_migrate_project_features.rb
@@ -3,7 +3,7 @@ class MigrateProjectFeatures < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON =
- <<-EOT
+ <<-EOT.freeze
Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
a new table called project_features.
EOT
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index d10f3a6d104..e20e693f3aa 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -5,9 +5,15 @@ class AddGroupIdToLabels < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :labels, :group_id, :integer
add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
add_concurrent_index :labels, :group_id
end
+
+ def down
+ remove_foreign_key :labels, column: :group_id
+ remove_index :labels, :group_id if index_exists? :labels, :group_id
+ remove_column :labels, :group_id
+ end
end
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
index b5de552b98c..19f7b1076a7 100644
--- a/db/migrate/20160920160832_add_index_to_labels_title.rb
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsTitle < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, :title
end
+
+ def down
+ remove_index :labels, :title if index_exists? :labels, :title
+ end
end
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
index e875213ab96..9f502a8df73 100644
--- a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -71,7 +71,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
'StuckCiBuildsWorker' => :cronjob,
'UpdateMergeRequestsWorker' => :update_merge_requests
}
- }
+ }.freeze
def up
Sidekiq.redis do |redis|
@@ -93,7 +93,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
def migrate_from_queue(redis, queue, job_mapping)
while job = redis.lpop("queue:#{queue}")
- payload = JSON.load(job)
+ payload = JSON.parse(job)
new_queue = job_mapping[payload['class']]
# If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
index 2abfe47b776..35ad22b6c01 100644
--- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -25,9 +25,15 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
# comments:
# disable_ddl_transaction!
- def change
+ def up
add_column :merge_request_metrics, :pipeline_id, :integer
- add_concurrent_index :merge_request_metrics, :pipeline_id
add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
+ add_concurrent_index :merge_request_metrics, :pipeline_id
+ end
+
+ def down
+ remove_foreign_key :merge_request_metrics, column: :pipeline_id
+ remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id
+ remove_column :merge_request_metrics, :pipeline_id
end
end
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
index 06d07bdb835..fc2e4c12b30 100644
--- a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -25,7 +25,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
incoming_email: {
'EmailReceiverWorker' => :email_receiver
}
- }
+ }.freeze
def up
Sidekiq.redis do |redis|
@@ -47,7 +47,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
def migrate_from_queue(redis, queue, job_mapping)
while job = redis.lpop("queue:#{queue}")
- payload = JSON.load(job)
+ payload = JSON.parse(job)
new_queue = job_mapping[payload['class']]
# If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
index d5c343dc527..8b1c10a124f 100644
--- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -9,6 +9,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
end
def down
+ remove_foreign_key :subscriptions, column: :project_id
remove_column :subscriptions, :project_id
end
end
diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb
index 750a6a8c51e..94b8ddd46f5 100644
--- a/db/migrate/20161106185620_add_project_import_data_project_index.rb
+++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb
@@ -6,7 +6,11 @@ class AddProjectImportDataProjectIndex < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :project_import_data, :project_id
end
+
+ def down
+ remove_index :project_import_data, :project_id if index_exists? :project_import_data, :project_id
+ end
end
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
index eab74c01dfd..73f9d92bb22 100644
--- a/db/migrate/20161124111395_add_index_to_parent_id.rb
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -8,7 +8,11 @@ class AddIndexToParentId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index(:namespaces, [:parent_id, :id], unique: true)
end
+
+ def down
+ remove_index :namespaces, [:parent_id, :id] if index_exists? :namespaces, [:parent_id, :id]
+ end
end
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 77e0c40d850..e5292cfba07 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -12,7 +12,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]
+ Gitlab.config.repositories.storages[repository_storage]['path']
end
def repository_path
@@ -34,7 +34,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
new_jobs = []
while job = redis.lpop('queue:process_commit')
- payload = JSON.load(job)
+ payload = JSON.parse(job)
project = Project.find_including_path(payload['args'][0])
next unless project
@@ -75,7 +75,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
new_jobs = []
while job = redis.lpop('queue:process_commit')
- payload = JSON.load(job)
+ payload = JSON.parse(job)
payload['args'][2] = payload['args'][2]['id']
diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
index 9deab19782e..8100287ef48 100644
--- a/db/migrate/20161128142110_remove_unnecessary_indexes.rb
+++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
@@ -12,7 +12,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id)
remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id)
remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id)
- remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"])
+ remove_index :deployments, column: %w(project_id environment_id) if index_exists?(:deployments, %w(project_id environment_id))
remove_index :lists, column: :board_id if index_exists?(:lists, :board_id)
remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id)
remove_index :notes, column: :project_id if index_exists?(:notes, :project_id)
@@ -24,7 +24,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
add_concurrent_index :award_emoji, :user_id
add_concurrent_index :ci_builds, :commit_id
add_concurrent_index :deployments, :project_id
- add_concurrent_index :deployments, ["project_id", "environment_id"]
+ add_concurrent_index :deployments, %w(project_id environment_id)
add_concurrent_index :lists, :board_id
add_concurrent_index :milestones, :project_id
add_concurrent_index :notes, :project_id
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
index 3ae3f2c159b..82fbdf02444 100644
--- a/db/migrate/20161201160452_migrate_project_statistics.rb
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -16,8 +16,9 @@ class MigrateProjectStatistics < ActiveRecord::Migration
remove_column :projects, :commit_count
end
+ # rubocop: disable Migration/AddColumn
def down
- add_column_with_default :projects, :repository_size, :float, default: 0.0
- add_column_with_default :projects, :commit_count, :integer, default: 0
+ add_column :projects, :repository_size, :float, default: 0.0
+ add_column :projects, :commit_count, :integer, default: 0
end
end
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
index 4a51337bda6..6d6c8906204 100644
--- a/db/migrate/20161202152035_add_index_to_routes.rb
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -9,8 +9,13 @@ class AddIndexToRoutes < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index(:routes, :path, unique: true)
add_concurrent_index(:routes, [:source_type, :source_id], unique: true)
end
+
+ def down
+ remove_index(:routes, :path) if index_exists? :routes, :path
+ remove_index(:routes, [:source_type, :source_id]) if index_exists? :routes, [:source_type, :source_id]
+ end
end
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
index b74552e762d..a20a903a752 100644
--- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -42,10 +42,10 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
conflicts.each do |id, name|
update_sql =
Arel::UpdateManager.new(ActiveRecord::Base).
- table(environments).
- set(environments[:name] => name + "-" + id.to_s).
- where(environments[:id].eq(id)).
- to_sql
+ table(environments).
+ set(environments[:name] => name + "-" + id.to_s).
+ where(environments[:id].eq(id)).
+ to_sql
connection.exec_update(update_sql, self.class.name, [])
end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
index ac680c8d10f..5ff0f5bae4d 100644
--- a/db/migrate/20161207231621_create_environment_name_unique_index.rb
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -12,7 +12,7 @@ class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
end
def down
- remove_index :environments, [:project_id, :name], unique: true
- add_concurrent_index :environments, [:project_id, :name]
+ remove_index :environments, [:project_id, :name]
+ add_concurrent_index :environments, [:project_id, :name], unique: true
end
end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
index e9fcef1cd45..ede0316e860 100644
--- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -9,7 +9,11 @@ class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :environments, [:project_id, :slug], unique: true
end
+
+ def down
+ remove_index :environments, [:project_id, :slug] if index_exists? :environments, [:project_id, :slug]
+ end
end
diff --git a/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
new file mode 100644
index 00000000000..e63d5927f86
--- /dev/null
+++ b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
@@ -0,0 +1,37 @@
+class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :oauth_openid_requests do |t|
+ t.integer :access_grant_id, null: false
+ t.string :nonce, null: false
+ end
+
+ if Gitlab::Database.postgresql?
+ # add foreign key without validation to avoid downtime on PostgreSQL,
+ # also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
+ execute %q{
+ ALTER TABLE "oauth_openid_requests"
+ ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
+ FOREIGN KEY ("access_grant_id")
+ REFERENCES "oauth_access_grants" ("id")
+ NOT VALID;
+ }
+ else
+ execute %q{
+ ALTER TABLE oauth_openid_requests
+ ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id
+ FOREIGN KEY (access_grant_id)
+ REFERENCES oauth_access_grants (id);
+ }
+ end
+ end
+
+ def down
+ drop_table :oauth_openid_requests
+ end
+end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
index 6958500306f..53f4c6bbb18 100644
--- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -17,6 +17,6 @@ class AddLowerPathIndexToRoutes < ActiveRecord::Migration
def down
return unless Gitlab::Database.postgresql?
- remove_index :routes, name: :index_on_routes_lower_path
+ remove_index :routes, name: :index_on_routes_lower_path if index_exists?(:routes, name: :index_on_routes_lower_path)
end
end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index 241afc6b097..8fb1f9d5e73 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -60,7 +60,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
def move_namespace(group_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]
+ Gitlab.config.repositories.storages[row['repository_storage']]['path']
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index a0ce927161f..61dcc8c54f5 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -71,7 +71,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
route_exists = route_exists?(path)
Gitlab.config.repositories.storages.each_value do |storage|
- if route_exists || path_exists?(path, storage)
+ if route_exists || path_exists?(path, storage['path'])
counter += 1
path = "#{base}#{counter}"
@@ -84,7 +84,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def move_namespace(namespace_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]
+ Gitlab.config.repositories.storages[row['repository_storage']]['path']
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb
new file mode 100644
index 00000000000..af1bac897cc
--- /dev/null
+++ b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ChangeExpiresAtToDateInPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because it alters expires_at column from datetime to date'
+
+ def up
+ change_column :personal_access_tokens, :expires_at, :date
+ end
+
+ def down
+ change_column :personal_access_tokens, :expires_at, :datetime
+ end
+end
diff --git a/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..ea9caceaa2c
--- /dev/null
+++ b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddImpersonationToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :personal_access_tokens, :impersonation
+ end
+end
diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb
new file mode 100644
index 00000000000..7995d383986
--- /dev/null
+++ b/db/migrate/20170120131253_create_chat_teams.rb
@@ -0,0 +1,18 @@
+class CreateChatTeams < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = "Adding a foreign key"
+
+ disable_ddl_transaction!
+
+ def change
+ create_table :chat_teams do |t|
+ t.references :namespace, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.string :team_id
+ t.string :name
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
deleted file mode 100644
index f90637e1e35..00000000000
--- a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class AddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
- disable_ddl_transaction!
-
- DOWNTIME = false
-
- def up
- add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
- end
-
- def down
- remove_column :users, :notified_of_own_activity
- end
-end
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
index 69bfa2d3fc4..a7d4e141a1a 100644
--- a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -49,6 +49,9 @@ class AddForeignKeysToTimelogs < ActiveRecord::Migration
Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
+ remove_foreign_key :timelogs, name: 'fk_timelogs_issues_issue_id'
+ remove_foreign_key :timelogs, name: 'fk_timelogs_merge_requests_merge_request_id'
+
remove_columns :timelogs, :issue_id, :merge_request_id
end
end
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
index e9a0aee4d6a..629b49436e3 100644
--- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -8,4 +8,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
def up
add_concurrent_index(:project_authorizations, :project_id)
end
+
+ def down
+ remove_index(:project_authorizations, :project_id) if
+ Gitlab::Database.postgresql?
+ end
end
diff --git a/db/migrate/20170130221926_create_uploads.rb b/db/migrate/20170130221926_create_uploads.rb
new file mode 100644
index 00000000000..6f06c5dd840
--- /dev/null
+++ b/db/migrate/20170130221926_create_uploads.rb
@@ -0,0 +1,20 @@
+class CreateUploads < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :uploads do |t|
+ t.integer :size, limit: 8, null: false
+ t.string :path, null: false
+ t.string :checksum, limit: 64
+ t.references :model, polymorphic: true
+ t.string :uploader, null: false
+ t.datetime :created_at, null: false
+ end
+
+ add_index :uploads, :path
+ add_index :uploads, :checksum
+ add_index :uploads, [:model_id, :model_type]
+ end
+end
diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb
new file mode 100644
index 00000000000..1baad0893e3
--- /dev/null
+++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRelativePositionToIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_column :issues, :relative_position, :integer
+
+ add_concurrent_index :issues, :relative_position
+ end
+
+ def down
+ remove_column :issues, :relative_position
+
+ remove_index :issues, :relative_position if index_exists? :issues, :relative_position
+ end
+end
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
index 8f944930807..31ef458c44f 100644
--- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, [:type, :project_id]
end
+
+ def down
+ remove_index :labels, [:type, :project_id] if index_exists? :labels, [:type, :project_id]
+ end
end
diff --git a/db/migrate/20170206115204_add_column_ghost_to_users.rb b/db/migrate/20170206115204_add_column_ghost_to_users.rb
new file mode 100644
index 00000000000..cc1eeda1160
--- /dev/null
+++ b/db/migrate/20170206115204_add_column_ghost_to_users.rb
@@ -0,0 +1,11 @@
+class AddColumnGhostToUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column :users, :ghost, :boolean
+ end
+
+ def down
+ remove_column :users, :ghost
+ end
+end
diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
index f922ed209aa..70fb0ef12f9 100644
--- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
+++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
@@ -5,8 +5,13 @@ class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, :title
add_concurrent_index :labels, :project_id
end
+
+ def down
+ remove_index :labels, :title if index_exists? :labels, :title
+ remove_index :labels, :project_id if index_exists? :labels, :project_id
+ end
end
diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
index 61e49c14fc0..07d4f8af27f 100644
--- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -5,7 +5,11 @@ class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_trigger_requests, :commit_id
end
+
+ def down
+ remove_index :ci_trigger_requests, :commit_id if index_exists? :ci_trigger_requests, :commit_id
+ end
end
diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
index c01753cfbd2..2d8329b7862 100644
--- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
@@ -8,7 +8,11 @@ class AddIndexToUserAgentDetail < ActiveRecord::Migration
disable_ddl_transaction!
- def change
- add_concurrent_index(:user_agent_details, [:subject_id, :subject_type])
+ def up
+ add_concurrent_index :user_agent_details, [:subject_id, :subject_type]
+ end
+
+ def down
+ remove_index :user_agent_details, [:subject_id, :subject_type] if index_exists? :user_agent_details, [:subject_id, :subject_type]
end
end
diff --git a/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb
new file mode 100644
index 00000000000..9ab970134be
--- /dev/null
+++ b/db/migrate/20170210131347_add_unique_ips_limit_to_application_settings.rb
@@ -0,0 +1,17 @@
+class AddUniqueIpsLimitToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_column :application_settings, :unique_ips_limit_per_user, :integer
+ add_column :application_settings, :unique_ips_limit_time_window, :integer
+ add_column_with_default :application_settings, :unique_ips_limit_enabled, :boolean, default: false
+ end
+
+ def down
+ remove_column :application_settings, :unique_ips_limit_per_user
+ remove_column :application_settings, :unique_ips_limit_time_window
+ remove_column :application_settings, :unique_ips_limit_enabled
+ end
+end
diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
new file mode 100644
index 00000000000..e0e3ff8957a
--- /dev/null
+++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
@@ -0,0 +1,11 @@
+class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings,
+ :default_artifacts_expire_in, :string,
+ null: false, default: '0'
+ end
+end
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
new file mode 100644
index 00000000000..8a96a784c97
--- /dev/null
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -0,0 +1,14 @@
+class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
+ end
+
+ def down
+ remove_index :ci_commits, [:gl_project_id, :ref, :status] if index_exists? :ci_commits, [:gl_project_id, :ref, :status]
+ end
+end
diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
new file mode 100644
index 00000000000..a2839f52d89
--- /dev/null
+++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
@@ -0,0 +1,8 @@
+class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def change
+ remove_index(:ci_commits, column: [:gl_project_id, :status])
+ end
+end
diff --git a/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb
new file mode 100644
index 00000000000..9011526565d
--- /dev/null
+++ b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameMergeWhenBuildSucceeds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Renaming the column merge_when_build_succeeds'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ rename_column :merge_requests, :merge_when_build_succeeds, :merge_when_pipeline_succeeds
+ end
+end
diff --git a/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb
new file mode 100644
index 00000000000..b2b68ff72d1
--- /dev/null
+++ b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameOnlyAllowMergeIfBuildSucceeds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Renaming the column only_allow_merge_if_build_succeeds'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ rename_column :projects, :only_allow_merge_if_build_succeeds, :only_allow_merge_if_pipeline_succeeds
+ end
+end
diff --git a/db/migrate/20170217151948_add_owner_id_to_triggers.rb b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
new file mode 100644
index 00000000000..16d7cc5bed6
--- /dev/null
+++ b/db/migrate/20170217151948_add_owner_id_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddOwnerIdToTriggers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_triggers, :owner_id, :integer
+ end
+end
diff --git a/db/migrate/20170217151949_add_description_to_triggers.rb b/db/migrate/20170217151949_add_description_to_triggers.rb
new file mode 100644
index 00000000000..1dca0e37412
--- /dev/null
+++ b/db/migrate/20170217151949_add_description_to_triggers.rb
@@ -0,0 +1,9 @@
+class AddDescriptionToTriggers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_triggers, :description, :string
+ end
+end
diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb
new file mode 100644
index 00000000000..4db8658f36f
--- /dev/null
+++ b/db/migrate/20170222143317_drop_ci_projects.rb
@@ -0,0 +1,34 @@
+class DropCiProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ drop_table :ci_projects
+ end
+
+ def down
+ create_table "ci_projects", force: :cascade do |t|
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+ end
+end
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
new file mode 100644
index 00000000000..eac93e8e407
--- /dev/null
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -0,0 +1,28 @@
+class RemoveOldProjectIdColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Unused columns are being removed.'
+
+ def up
+ remove_index :ci_builds, :project_id if
+ index_exists?(:ci_builds, :project_id)
+
+ remove_column :ci_builds, :project_id
+ remove_column :ci_commits, :project_id
+ remove_column :ci_runner_projects, :project_id
+ remove_column :ci_triggers, :project_id
+ remove_column :ci_variables, :project_id
+ end
+
+ def down
+ add_column :ci_builds, :project_id, :integer
+ add_column :ci_commits, :project_id, :integer
+ add_column :ci_runner_projects, :project_id, :integer
+ add_column :ci_triggers, :project_id, :integer
+ add_column :ci_variables, :project_id, :integer
+
+ add_concurrent_index :ci_builds, :project_id
+ end
+end
diff --git a/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
new file mode 100644
index 00000000000..7c19d471557
--- /dev/null
+++ b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
@@ -0,0 +1,14 @@
+class RenameGlProjectIdToProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming an actively used column.'
+
+ def change
+ rename_column :ci_builds, :gl_project_id, :project_id
+ rename_column :ci_commits, :gl_project_id, :project_id
+ rename_column :ci_runner_projects, :gl_project_id, :project_id
+ rename_column :ci_triggers, :gl_project_id, :project_id
+ rename_column :ci_variables, :gl_project_id, :project_id
+ end
+end
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
new file mode 100644
index 00000000000..f54608ecceb
--- /dev/null
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:projects, :printing_merge_request_link_enabled, :boolean, default: true)
+ end
+
+ def down
+ remove_column(:projects, :printing_merge_request_link_enabled)
+ end
+end
diff --git a/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
new file mode 100644
index 00000000000..4f061d96392
--- /dev/null
+++ b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
@@ -0,0 +1,10 @@
+class RenameCiCommitsToCiPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Rename table ci_commits to ci_pipelines'
+
+ def change
+ rename_table 'ci_commits', 'ci_pipelines'
+ end
+end
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
new file mode 100644
index 00000000000..1e2abea5254
--- /dev/null
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -0,0 +1,83 @@
+class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON =
+ 'Remove unused columns in used tables.' \
+ ' Downtime required in case Rails caches them'
+
+ def up
+ %w[ci_application_settings
+ ci_events
+ ci_jobs
+ ci_sessions
+ ci_taggings
+ ci_tags].each do |table|
+ drop_table(table)
+ end
+
+ remove_column :ci_pipelines, :push_data, :text
+ remove_column :ci_builds, :job_id, :integer
+ remove_column :ci_builds, :deploy, :boolean
+ end
+
+ def down
+ add_column :ci_builds, :deploy, :boolean
+ add_column :ci_builds, :job_id, :integer
+ add_column :ci_pipelines, :push_data, :text
+
+ create_table "ci_tags", force: :cascade do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ create_table "ci_taggings", force: :cascade do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", %w[taggable_id taggable_type context]
+
+ create_table "ci_sessions", force: :cascade do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_jobs", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ create_table "ci_events", force: :cascade do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_application_settings", force: :cascade do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ end
+end
diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
new file mode 100644
index 00000000000..5fbdc45f1a7
--- /dev/null
+++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
@@ -0,0 +1,15 @@
+class AddOwnerIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key :ci_triggers, column: :owner_id
+ end
+end
diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb
new file mode 100644
index 00000000000..c429039c275
--- /dev/null
+++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb
@@ -0,0 +1,24 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToUserGhost < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :users, :ghost
+ end
+
+ def down
+ remove_index :users, :ghost
+ end
+end
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
new file mode 100644
index 00000000000..b39c0a3be0f
--- /dev/null
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -0,0 +1,24 @@
+class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ if our_column_exists?
+ remove_column :users, :notified_of_own_activity
+ end
+ end
+
+ def down
+ unless our_column_exists?
+ add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
+ end
+ end
+
+ private
+
+ def our_column_exists?
+ column_exists?(:users, :notified_of_own_activity)
+ end
+end
diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb
new file mode 100644
index 00000000000..1326118cc8d
--- /dev/null
+++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb
@@ -0,0 +1,7 @@
+class AddClosedAtToIssues < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :issues, :closed_at, :datetime
+ end
+end
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
index df38591a333..14b5ef476f0 100644
--- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -14,15 +14,15 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
finder_sql =
projects.
- join(namespaces, Arel::Nodes::InnerJoin).
- on(projects[:namespace_id].eq(namespaces[:id])).
- where(projects[:visibility_level].gt(namespaces[:visibility_level])).
- project(projects[:id], namespaces[:visibility_level]).
- take(BATCH_SIZE).
- to_sql
+ join(namespaces, Arel::Nodes::InnerJoin).
+ on(projects[:namespace_id].eq(namespaces[:id])).
+ where(projects[:visibility_level].gt(namespaces[:visibility_level])).
+ project(projects[:id], namespaces[:visibility_level]).
+ take(BATCH_SIZE).
+ to_sql
# Update matching rows in batches. Each batch can cause up to 3 UPDATE
- # statements, in addition to the SELECT: one per visibility_level
+ # statements, in addition to the SELECT: one per visibility_level
loop do
to_update = connection.exec_query(finder_sql)
break if to_update.rows.count == 0
diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
index 282837be1fa..49a6bc884a8 100644
--- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb
+++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
@@ -37,7 +37,7 @@ class RenameReservedProjectNames < ActiveRecord::Migration
unsubscribes
update
users
- wikis)
+ wikis).freeze
def up
queues = Array.new(THREAD_COUNT) { Queue.new }
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
new file mode 100644
index 00000000000..b518038e93a
--- /dev/null
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # This ensures we don't lock all users for the duration of the migration.
+ update_column_in_batches(:users, :authorized_projects_populated, nil)
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
index 89aa753646c..aee0c1b6245 100644
--- a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -18,6 +18,7 @@ class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
# disable_ddl_transaction!
def change
- remove_columns :timelogs, :trackable_id, :trackable_type
+ remove_column :timelogs, :trackable_id, :integer
+ remove_column :timelogs, :trackable_type, :string
end
end
diff --git a/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
new file mode 100644
index 00000000000..e206f9af636
--- /dev/null
+++ b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
@@ -0,0 +1,20 @@
+class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute %q{
+ ALTER TABLE "oauth_openid_requests"
+ VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id";
+ }
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
index 84954b1ef64..603efc43782 100644
--- a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
+++ b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb
@@ -1,10 +1,8 @@
class DisableInvalidServiceTemplates < ActiveRecord::Migration
DOWNTIME = false
- unless defined?(Service)
- class Service < ActiveRecord::Base
- self.inheritance_column = nil
- end
+ class Service < ActiveRecord::Base
+ self.inheritance_column = nil
end
def up
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
new file mode 100644
index 00000000000..2dd14ee5a78
--- /dev/null
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -0,0 +1,87 @@
+class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database
+
+ DOWNTIME = false
+
+ def up
+ Gitlab::Database.with_connection_pool(2) do |pool|
+ threads = []
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(<<-SQL.strip_heredoc)
+ UPDATE services
+ SET properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type IN
+ ('SlackService', 'MattermostService', 'HipchatService')
+ AND build_events = #{true_value};
+ SQL
+ end
+ end
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(update_pipeline_services_sql)
+ end
+ end
+
+ threads.each(&:join)
+ end
+ end
+
+ def down
+ # Don't bother to migrate the data back
+ end
+
+ def connection
+ # Rails memoizes connection objects, but this causes them to be shared
+ # amongst threads; we don't want that.
+ Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
+ end
+
+ private
+
+ def update_pipeline_services_sql
+ if Gitlab::Database.postgresql?
+ <<-SQL
+ UPDATE services
+ SET type = 'PipelinesEmailService'
+ , properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type = 'BuildsEmailService'
+ AND
+ (SELECT 1 FROM services pipeline_services
+ WHERE pipeline_services.project_id = services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService' LIMIT 1)
+ IS NULL;
+ SQL
+ else
+ <<-SQL
+ UPDATE services build_services
+ LEFT OUTER JOIN services pipeline_services
+ ON build_services.project_id = pipeline_services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService'
+ SET build_services.type = 'PipelinesEmailService'
+ , build_services.properties = replace(build_services.properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , build_services.pipeline_events = #{true_value}
+ , build_services.build_events = #{false_value}
+ WHERE build_services.type = 'BuildsEmailService'
+ AND pipeline_services.id IS NULL;
+ SQL
+ end.strip_heredoc
+ end
+end
diff --git a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
new file mode 100644
index 00000000000..ec6e8cdfc45
--- /dev/null
+++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
@@ -0,0 +1,23 @@
+class MigrateLegacyManualActions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ disable_statement_timeout
+
+ execute <<-EOS
+ UPDATE ci_builds SET status = 'manual', allow_failure = true
+ WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped';
+ EOS
+ end
+
+ def down
+ disable_statement_timeout
+
+ execute <<-EOS
+ UPDATE ci_builds SET status = 'skipped', allow_failure = false
+ WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual';
+ EOS
+ end
+end
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
new file mode 100644
index 00000000000..b61dd7cfc61
--- /dev/null
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetRelativePositionForIssue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:issues, :relative_position, nil) do |table, query|
+ query.where(table[:relative_position].not_eq(nil))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
new file mode 100644
index 00000000000..44c688fa134
--- /dev/null
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -0,0 +1,72 @@
+require 'thread'
+
+class RenameMoreReservedProjectNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ DOWNTIME = false
+
+ KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
+
+ def up
+ reserved_projects.each_slice(100) do |slice|
+ rename_projects(slice)
+ end
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def reserved_projects
+ Project.unscoped.
+ includes(:namespace).
+ where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)').
+ where('projects.path' => KNOWN_PATHS)
+ end
+
+ def route_exists?(full_path)
+ quoted_path = ActiveRecord::Base.connection.quote_string(full_path)
+
+ ActiveRecord::Base.connection.
+ select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present?
+ end
+
+ # Adds number to the end of the path that is not taken by other route
+ def rename_path(namespace_path, path_was)
+ counter = 0
+ path = "#{path_was}#{counter}"
+
+ while route_exists?("#{namespace_path}/#{path}")
+ counter += 1
+ path = "#{path_was}#{counter}"
+ end
+
+ path
+ end
+
+ def rename_projects(projects)
+ projects.each do |project|
+ id = project.id
+ path_was = project.path
+ namespace_path = project.namespace.path
+ path = rename_path(namespace_path, path_was)
+
+ begin
+ # Because project path update is quite complex operation we can't safely
+ # copy-paste all code from GitLab. As exception we use Rails code here
+ project.rename_repo if rename_project_row(project, path)
+ rescue Exception => e # rubocop: disable Lint/RescueException
+ Rails.logger.error "Exception when renaming project #{id}: #{e.message}"
+ end
+ end
+ end
+
+ def rename_project_row(project, path)
+ project.respond_to?(:update_attributes) &&
+ project.update_attributes(path: path) &&
+ project.respond_to?(:rename_repo)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 08d11546800..db57fb0a548 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170215200045) do
+ActiveRecord::Schema.define(version: 20170315194013) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -112,6 +112,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.string "plantuml_url"
t.boolean "plantuml_enabled"
t.integer "terminal_max_session_time", default: 0, null: false
+ t.integer "max_pages_size", default: 100, null: false
+ t.string "default_artifacts_expire_in", default: "0", null: false
+ t.integer "unique_ips_limit_per_user"
+ t.integer "unique_ips_limit_time_window"
+ t.boolean "unique_ips_limit_enabled", default: false, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -172,15 +177,17 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
- create_table "ci_application_settings", force: :cascade do |t|
- t.boolean "all_broken_builds"
- t.boolean "add_pusher"
- t.datetime "created_at"
- t.datetime "updated_at"
+ create_table "chat_teams", force: :cascade do |t|
+ t.integer "namespace_id", null: false
+ t.string "team_id"
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
+ add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+
create_table "ci_builds", force: :cascade do |t|
- t.integer "project_id"
t.string "status"
t.datetime "finished_at"
t.text "trace"
@@ -191,9 +198,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.float "coverage"
t.integer "commit_id"
t.text "commands"
- t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
t.text "options"
t.boolean "allow_failure", default: false, null: false
t.string "stage"
@@ -206,7 +211,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.string "target_url"
t.string "description"
t.text "artifacts_file"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
@@ -225,25 +230,22 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
- create_table "ci_commits", force: :cascade do |t|
- t.integer "project_id"
+ create_table "ci_pipelines", force: :cascade do |t|
t.string "ref"
t.string "sha"
t.string "before_sha"
- t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.string "status"
t.datetime "started_at"
t.datetime "finished_at"
@@ -252,67 +254,20 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.integer "lock_version"
end
- add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
- add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
- add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
- add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
- add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
-
- create_table "ci_events", force: :cascade do |t|
- t.integer "project_id"
- t.integer "user_id"
- t.integer "is_admin"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
- t.text "commands"
- t.boolean "active", default: true, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
- t.datetime "deleted_at"
- end
-
- create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
- t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
- t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
- t.text "generated_yaml_config"
- end
+ add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+ add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+ add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
+ add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
- t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
@@ -336,30 +291,6 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
- t.text "data"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context", limit: 128
- t.datetime "created_at"
- end
-
- add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
- create_table "ci_tags", force: :cascade do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
- end
-
create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
@@ -372,26 +303,26 @@ ActiveRecord::Schema.define(version: 20170215200045) do
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
- t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
+ t.integer "owner_id"
+ t.string "description"
end
- add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
+ add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
- t.integer "project_id"
t.string "key"
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
create_table "container_images", force: :cascade do |t|
t.integer "project_id"
@@ -520,6 +451,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
+ t.integer "relative_position"
+ t.datetime "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -531,6 +464,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
+ add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -694,7 +628,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.integer "updated_by_id"
t.text "merge_error"
t.text "merge_params"
- t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.boolean "merge_when_pipeline_succeeds", default: false, null: false
t.integer "merge_user_id"
t.string "merge_commit_sha"
t.datetime "deleted_at"
@@ -868,6 +802,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "oauth_openid_requests", force: :cascade do |t|
+ t.integer "access_grant_id", null: false
+ t.string "nonce", null: false
+ end
+
create_table "pages_domains", force: :cascade do |t|
t.integer "project_id"
t.text "certificate"
@@ -884,10 +823,11 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.string "token", null: false
t.string "name", null: false
t.boolean "revoked", default: false
- t.datetime "expires_at"
+ t.date "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false
+ t.boolean "impersonation", default: false, null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
@@ -977,7 +917,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
t.boolean "container_registry_enabled"
- t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "only_allow_merge_if_pipeline_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: false, null: false
@@ -985,6 +925,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
+ t.boolean "printing_merge_request_link_enabled", default: true, null: false
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1214,6 +1155,20 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
+ create_table "uploads", force: :cascade do |t|
+ t.integer "size", limit: 8, null: false
+ t.string "path", null: false
+ t.string "checksum", limit: 64
+ t.integer "model_id"
+ t.string "model_type"
+ t.string "uploader", null: false
+ t.datetime "created_at", null: false
+ end
+
+ add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
+ add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
+ add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
+
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
@@ -1286,7 +1241,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated"
- t.boolean "notified_of_own_activity", default: false, null: false
+ t.boolean "ghost"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1296,6 +1251,7 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+ add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -1337,16 +1293,19 @@ ActiveRecord::Schema.define(version: 20170215200045) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "boards", "projects"
+ add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+ add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
- add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
+ add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
+ add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 2712206373d..57d85d770e7 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -19,7 +19,7 @@
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
-- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat.
+- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
@@ -51,6 +51,7 @@
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
+- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md
new file mode 100644
index 00000000000..2c289c67a6d
--- /dev/null
+++ b/doc/administration/auth/crowd.md
@@ -0,0 +1,68 @@
+# Atlassian Crowd OmniAuth Provider
+
+## Configure a new Crowd application
+
+1. Choose 'Applications' in the top menu, then 'Add application'.
+1. Go through the 'Add application' steps, entering the appropriate details.
+ The screenshot below shows an example configuration.
+
+ ![Example Crowd application configuration](img/crowd_application.png)
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file.
+
+ **Omnibus:**
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ **Source:**
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. Add the provider configuration:
+
+ **Omnibus:**
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "crowd",
+ "args" => {
+ "crowd_server_url" => "CROWD_SERVER_URL",
+ "application_name" => "YOUR_APP_NAME",
+ "application_password" => "YOUR_APP_PASSWORD"
+ }
+ }
+ ]
+ ```
+
+ **Source:**
+
+ ```
+ - { name: 'crowd',
+ args: {
+ crowd_server_url: 'CROWD_SERVER_URL',
+ application_name: 'YOUR_APP_NAME',
+ application_password: 'YOUR_APP_PASSWORD' } }
+ ```
+1. Change `CROWD_SERVER_URL` to the URL of your Crowd server.
+1. Change `YOUR_APP_NAME` to the application name from Crowd applications page.
+1. Change `YOUR_APP_PASSWORD` to the application password you've set.
+1. Save the configuration file.
+1. [Reconfigure][] or [restart][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be a Crowd tab in the sign in form.
+
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/auth/img/crowd_application.png b/doc/administration/auth/img/crowd_application.png
new file mode 100644
index 00000000000..7deea9dac8e
--- /dev/null
+++ b/doc/administration/auth/img/crowd_application.png
Binary files differ
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 4d1cb391e69..dc4e57f25fb 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -483,12 +483,108 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
project, you can disable it from your project's settings. Read the user guide
on how to achieve that.
+## Disable Container Registry but use GitLab as an auth endpoint
+
+You can disable the embedded Container Registry to use an external one, but
+still use GitLab as an auth endpoint.
+
+**Omnibus GitLab**
+1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
+
+ ```ruby
+ registry['enable'] = false
+ gitlab_rails['registry_enabled'] = true
+ gitlab_rails['registry_host'] = "registry.gitlab.example.com"
+ gitlab_rails['registry_port'] = "5005"
+ gitlab_rails['registry_api_url'] = "http://localhost:5000"
+ gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
+ gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
+ gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`:
+
+ ```
+ ## Container Registry
+
+ registry:
+ enabled: true
+ host: "registry.gitlab.example.com"
+ port: "5005"
+ api_url: "http://localhost:5000"
+ path: /var/opt/gitlab/gitlab-rails/shared/registry
+ key: /var/opt/gitlab/gitlab-rails/certificate.key
+ issuer: omnibus-gitlab-issuer
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
infinite amount of Docker images with arbitrary sizes. This setting will be
configurable in future releases.
+## Configure Container Registry notifications
+
+You can configure the Container Registry to send webhook notifications in
+response to events happening within the registry.
+
+Read more about the Container Registry notifications config options in the
+[Docker Registry notifications documentation][notifications-config].
+
+>**Note:**
+Multiple endpoints can be configured for the Container Registry.
+
+
+**Omnibus GitLab installations**
+
+To configure a notification endpoint in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['notifications'] = [
+ {
+ 'name' => 'test_endpoint',
+ 'url' => 'https://gitlab.example.com/notify',
+ 'timeout' => '500ms',
+ 'threshold' => 5,
+ 'backoff' => '1s',
+ 'headers' => {
+ "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"]
+ }
+ }
+ ]
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the notification endpoint is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+notifications:
+ endpoints:
+ - name: alistener
+ disabled: false
+ url: https://my.listener.com/event
+ headers: <http.Header>
+ timeout: 500
+ threshold: 5
+ backoff: 1000
+```
+
## Changelog
**GitLab 8.8 ([source docs][8-8-docs])**
@@ -510,3 +606,5 @@ configurable in future releases.
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
[new-domain]: #configure-container-registry-under-its-own-domain
+[notifications-config]: https://docs.docker.com/registry/notifications/
+[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications \ No newline at end of file
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index e4f94eb7cb6..0a08591c3ce 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -16,7 +16,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
1. Configure the GitLab application servers with the appropriate details.
- This step is covered in [Configuring GitLab for HA](gitlab.md)
+ This step is covered in [Configuring GitLab for HA](gitlab.md).
## Configure using Omnibus
@@ -105,6 +105,8 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Exit the database prompt by typing `\q` and Enter.
1. Exit the `gitlab-psql` user by running `exit` twice.
1. Run `sudo gitlab-ctl reconfigure` a final time.
+1. Configure the GitLab application servers with the appropriate details.
+ This step is covered in [Configuring GitLab for HA](gitlab.md).
---
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index dad8e956c0e..3245988fc14 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -19,8 +19,8 @@ you need to use with GitLab.
## GitLab Pages Ports
If you're using GitLab Pages you will need some additional port configurations.
-GitLab Pages requires a separate VIP. Configure DNS to point the
-`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the
+GitLab Pages requires a separate virtual IP address. Configure DNS to point the
+`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[GitLab Pages documentation][gitlab-pages] for more information.
| LB Port | Backend Port | Protocol |
@@ -32,7 +32,7 @@ GitLab Pages requires a separate VIP. Configure DNS to point the
Some organizations have policies against opening SSH port 22. In this case,
it may be helpful to configure an alternate SSH hostname that allows users
-to use SSH on port 443. An alternate SSH hostname will require a new VIP
+to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
compared to the other GitLab HTTP configuration above.
Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
index 86ef9d167e2..edb9c911aac 100644
--- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
+++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
@@ -13,7 +13,7 @@ To enable the GitLab monitor exporter:
1. Add or find and uncomment the following line, making sure it's set to `true`:
```ruby
- gitlab_monitor_exporter['enable'] = true
+ gitlab_monitor['enable'] = true
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 3a394c561db..b2445d1c0e5 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -3,10 +3,9 @@
>**Notes:**
- Prometheus and the various exporters listed in this page are bundled in the
Omnibus GitLab package. Check each exporter's documentation for the timeline
- they got added. For installations from source you will have to install
- them yourself. Over subsequent releases additional GitLab metrics will be
- captured.
-- Prometheus services are off by default but will be on starting with GitLab 9.0.
+ they got added. For installations from source you will have to install them
+ yourself. Over subsequent releases additional GitLab metrics will be captured.
+- Prometheus services are on by default with GitLab 9.0.
- Prometheus and its exporters do not authenticate users, and will be available
to anyone who can access them.
@@ -26,26 +25,25 @@ dashboard tool like [Grafana].
## Configuring Prometheus
>**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
+For installations from source you'll have to install and configure it yourself.
-To enable Prometheus:
+Prometheus and it's exporters are on by default, starting with GitLab 9.0.
+Prometheus will run as the `gitlab-prometheus` user and listen on
+`http://localhost:9090`. Each exporter will be automatically be set up as a
+monitoring target for Prometheus, unless individually disabled.
+
+To disable Prometheus and all of its exporters, as well as any added in the future:
1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line, making sure it's set to `true`:
+1. Add or find and uncomment the following line, making sure it's set to `false`:
```ruby
- prometheus['enable'] = true
+ prometheus_monitoring['enable'] = false
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
-By default, Prometheus will run as the `gitlab-prometheus` user and listen on
-`http://localhost:9090`. If the [node exporter](#node-exporter) service
-has been enabled, it will automatically be set up as a monitoring target for
-Prometheus.
-
## Changing the port Prometheus listens on
>**Note:**
@@ -71,16 +69,14 @@ To change the address/port that Prometheus listens on:
## Viewing performance metrics
-After you have [enabled Prometheus](#configuring-prometheus), you can visit
-`http://localhost:9090` for the dashboard that Prometheus offers by default.
+You can visit `http://localhost:9090` for the dashboard that Prometheus offers by default.
>**Note:**
If SSL has been enabled on your GitLab instance, you may not be able to access
Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are
some workarounds: using a separate browser for Prometheus, resetting HSTS, or
-having [Nginx proxy it][nginx-custom-config]. Follow issue [#27069] for more
-information.
+having [Nginx proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the
Prometheus console or through a compatible dashboard tool.
@@ -96,6 +92,24 @@ Sample Prometheus queries:
- **Data transmitted:** `irate(node_network_transmit_bytes[5m])`
- **Data received:** `irate(node_network_receive_bytes[5m])`
+## Configuring Prometheus to monitor Kubernetes
+
+> Introduced in GitLab 9.0.
+
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes in the cluster including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
+
+To disable the monitoring of Kubernetes:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line and set it to `false`:
+
+ ```ruby
+ prometheus['monitor_kubernetes'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
## Prometheus exporters
There are a number of libraries and servers which help in exporting existing
@@ -143,5 +157,6 @@ The GitLab monitor exporter allows you to measure various GitLab metrics.
[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
-[#27069]: https://gitlab.com/gitlab-org/gitlab-ce/issues/27069
[1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
+[prometheus integration]: ../../../user/project/integrations/prometheus.md
+[prometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 8de0cc5af5c..0c63b0b59a7 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -26,22 +26,24 @@ it works.
---
-In the case of custom domains, the Pages daemon needs to listen on ports `80`
-and/or `443`. For that reason, there is some flexibility in the way which you
-can set it up:
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
+which you can set it up:
-1. Run the pages daemon in the same server as GitLab, listening on a secondary IP.
-1. Run the pages daemon in a separate server. In that case, the
+1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
+1. Run the Pages daemon in a separate server. In that case, the
[Pages path](#change-storage-path) must also be present in the server that
- the pages daemon is installed, so you will have to share it via network.
-1. Run the pages daemon in the same server as GitLab, listening on the same IP
+ the Pages daemon is installed, so you will have to share it via network.
+1. Run the Pages daemon in the same server as GitLab, listening on the same IP
but on different ports. In that case, you will have to proxy the traffic with
a loadbalancer. If you choose that route note that you should use TCP load
balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
pages will not be able to be served with user provided certificates. For
HTTP it's OK to use HTTP or TCP load balancing.
-In this document, we will proceed assuming the first option.
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
## Prerequisites
@@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to:
serve Pages under HTTPS.
1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
so that your users don't have to bring their own.
+1. (Only for custom domains) Have a **secondary IP**.
### DNS configuration
@@ -62,11 +65,13 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
-*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN AAAA 2001::1
```
where `example.io` is the domain under which GitLab Pages will be served
-and `1.1.1.1` is the IP address of your GitLab instance.
+and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the
+IPv6 address. If you don't have IPv6, you can omit the AAAA record.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
@@ -102,6 +107,8 @@ The Pages daemon doesn't listen to the outside world.
1. [Reconfigure GitLab][reconfigure]
+Watch the [video tutorial][video-admin] for this configuration.
+
### Wildcard domains with TLS support
>**Requirements:**
@@ -136,7 +143,8 @@ outside world.
In addition to the wildcard domains, you can also have the option to configure
GitLab Pages to work with custom domains. Again, there are two options here:
support custom domains with and without TLS certificates. The easiest setup is
-that without TLS certificates.
+that without TLS certificates. In either case, you'll need a secondary IP. If
+you have IPv6 as well as IPv4 addresses, you can use them both.
### Custom domains
@@ -148,7 +156,7 @@ that without TLS certificates.
>
URL scheme: `http://page.example.io` and `http://domain.com`
-In that case, the pages daemon is running, Nginx still proxies requests to
+In that case, the Pages daemon is running, Nginx still proxies requests to
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains are supported, but no TLS.
@@ -158,11 +166,12 @@ world. Custom domains are supported, but no TLS.
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
- gitlab_pages['external_http'] = '1.1.1.2:80'
+ gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+ `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
+ listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
@@ -177,7 +186,7 @@ world. Custom domains are supported, but no TLS.
>
URL scheme: `https://page.example.io` and `https://domain.com`
-In that case, the pages daemon is running, Nginx still proxies requests to
+In that case, the Pages daemon is running, Nginx still proxies requests to
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
@@ -189,12 +198,13 @@ world. Custom domains and TLS are supported.
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
- gitlab_pages['external_http'] = '1.1.1.2:80'
- gitlab_pages['external_https'] = '1.1.1.2:443'
+ gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
+ gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+ `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
+ listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
@@ -270,3 +280,4 @@ latest previous version.
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[video-admin]: https://youtu.be/dD8c7WNcc6s
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 463715e48ca..a45c3306457 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -1,5 +1,9 @@
# GitLab Pages administration for source installations
+>**Note:**
+Before attempting to enable GitLab Pages, first make sure you have
+[installed GitLab](../../install/installation.md) successfully.
+
This is the documentation for configuring a GitLab Pages when you have installed
GitLab from source and not using the Omnibus packages.
@@ -13,18 +17,47 @@ Pages to the latest supported version.
## Overview
-[Read the Omnibus overview section.](index.md#overview)
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
-## Prerequisites
+---
-Before proceeding with the Pages configuration, you will need to:
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
+which you can set it up:
+
+1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
+1. Run the Pages daemon in a separate server. In that case, the
+ [Pages path](#change-storage-path) must also be present in the server that
+ the Pages daemon is installed, so you will have to share it via network.
+1. Run the Pages daemon in the same server as GitLab, listening on the same IP
+ but on different ports. In that case, you will have to proxy the traffic with
+ a loadbalancer. If you choose that route note that you should use TCP load
+ balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+ pages will not be able to be served with user provided certificates. For
+ HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
-1. Have a separate domain under which the GitLab Pages will be served. In this
- document we assume that to be `example.io`.
-1. Configure a **wildcard DNS record**.
-1. (Optional) Have a **wildcard certificate** for that domain if you decide to
- serve Pages under HTTPS.
-1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
+## Prerequisites
+
+Before proceeding with the Pages configuration, make sure that:
+
+1. You have a separate domain under which GitLab Pages will be served. In
+ this document we assume that to be `example.io`.
+1. You have configured a **wildcard DNS record** for that domain.
+1. You have installed the `zip` and `unzip` packages in the same server that
+ GitLab is installed since they are needed to compress/uncompress the
+ Pages artifacts.
+1. (Optional) You have a **wildcard certificate** for the Pages domain if you
+ decide to serve Pages (`*.example.io`) under HTTPS.
+1. (Optional but recommended) You have configured and enabled the [Shared Runners][]
so that your users don't have to bring their own.
### DNS configuration
@@ -72,7 +105,7 @@ The Pages daemon doesn't listen to the outside world.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -97,14 +130,21 @@ The Pages daemon doesn't listen to the outside world.
https: false
```
-1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` must match the `host` setting that you set above.
- ```bash
- sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
- sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+1. Copy the `gitlab-pages` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
+ ```
1. Restart NGINX
1. [Restart GitLab][restart]
@@ -128,7 +168,7 @@ outside world.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -146,6 +186,17 @@ outside world.
https: true
```
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` must match the `host` setting that you set above.
+ The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+ of the `example.io` domain:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash
@@ -153,12 +204,9 @@ outside world.
sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Restart NGINX
1. [Restart GitLab][restart]
-
## Advanced configuration
In addition to the wildcard domains, you can also have the option to configure
@@ -186,7 +234,7 @@ world. Custom domains are supported, but no TLS.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -221,12 +269,10 @@ world. Custom domains are supported, but no TLS.
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash
- sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
- sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
@@ -254,7 +300,7 @@ world. Custom domains and TLS are supported.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -297,8 +343,6 @@ world. Custom domains and TLS are supported.
sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
@@ -389,4 +433,6 @@ than GitLab to prevent XSS attacks.
[pages-userguide]: ../../user/project/pages/index.md
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
-[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.4.0
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example
+[shared runners]: ../../ci/runners/README.md
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 00494e7e9d6..e99a7ee29cc 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -13,7 +13,8 @@ three strategies for this feature:
### Email sub-addressing
-**If your provider or server supports email sub-addressing, we recommend using it.**
+**If your provider or server supports email sub-addressing, we recommend using it.
+Some features (e.g. create new issue via email) only work with sub-addressing.**
[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
a feature where any email to `user+some_arbitrary_tag@example.com` will end up
@@ -69,7 +70,9 @@ please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
-and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
+or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
+and use [an application password](https://support.google.com/mail/answer/185833).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
@@ -138,12 +141,32 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
```
+
+ ```ruby
+ # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+ gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
+
+ # Email account username
+ # Typically this is the userPrincipalName (UPN)
+ gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "exchange.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ ```
-1. Reconfigure GitLab and restart mailroom for the changes to take effect:
+1. Reconfigure GitLab for the changes to take effect:
```sh
sudo gitlab-ctl reconfigure
- sudo gitlab-ctl restart mailroom
```
1. Verify that everything is configured correctly:
@@ -230,6 +253,35 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
idle_timeout: 60
```
+
+ ```yaml
+ # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
+ incoming_email:
+ enabled: true
+
+ # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+ address: "incoming@exchange.example.com"
+
+ # Email account username
+ # Typically this is the userPrincipalName (UPN)
+ user: "incoming@ad-domain.example.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "exchange.example.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
+ ```
1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index d6aa6101026..55a45119525 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -52,9 +52,12 @@ respectively.
# Paths where repositories can be stored. Give the canonicalized absolute pathname.
# NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
storages: # You must have at least a 'default' storage path.
- default: /home/git/repositories
- nfs: /mnt/nfs/repositories
- cephfs: /mnt/cephfs/repositories
+ default:
+ path: /home/git/repositories
+ nfs:
+ path: /mnt/nfs/repositories
+ cephfs:
+ path: /mnt/cephfs/repositories
```
1. [Restart GitLab] for the changes to take effect.
@@ -75,9 +78,9 @@ working, you can remove the `repos_path` line.
```ruby
git_data_dirs({
- "default" => "/var/opt/gitlab/git-data",
- "nfs" => "/mnt/nfs/git-data",
- "cephfs" => "/mnt/cephfs/git-data"
+ "default" => { "path" => "/var/opt/gitlab/git-data" },
+ "nfs" => { "path" => "/mnt/nfs/git-data" },
+ "cephfs" => { "path" => "/mnt/cephfs/git-data" }
})
```
diff --git a/doc/api/README.md b/doc/api/README.md
index b334ca46caf..58d090b8f5e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -11,8 +11,6 @@ following locations:
- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
- [Broadcast Messages](broadcast_messages.md)
-- [Builds](builds.md)
-- [Build Triggers](build_triggers.md)
- [Build Variables](build_variables.md)
- [Commits](commits.md)
- [Deployments](deployments.md)
@@ -24,6 +22,7 @@ following locations:
- [Group Members](members.md)
- [Issues](issues.md)
- [Issue Boards](boards.md)
+- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
@@ -33,6 +32,7 @@ following locations:
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
- [Pipelines](pipelines.md)
+- [Pipeline Triggers](pipeline_triggers.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
- [Project Members](members.md)
@@ -89,7 +89,7 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i
Example of using the OAuth2 token in the header:
```shell
-curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
```
Read more about [GitLab as an OAuth2 client](oauth2.md).
@@ -127,13 +127,13 @@ is defined in [`lib/api.rb`][lib-api-url].
Example of a valid API request:
```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
+GET https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
```
Example of a valid API request using cURL and authentication via header:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
```
The API uses JSON to serialize data. You don't need to specify `.json` at the
@@ -159,6 +159,7 @@ The following table shows the possible return codes for API requests.
| Return values | Description |
| ------------- | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
+| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
@@ -206,7 +207,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API call and a request using cURL with sudo request,
@@ -217,9 +218,17 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v3/projects"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
```
+## Impersonation Tokens
+
+Impersonation Tokens are a type of Personal Access Token that can only be created by an admin for a specific user. These can be used by automated tools
+to authenticate with the API as a specific user, as a better alternative to using the user's password or private token directly, which may change over time,
+and to using the [Sudo](#sudo) feature, which requires the tool to know an admin's password or private token, which can change over time as well and are extremely powerful.
+
+For more information about the usage please refer to the [Users](users.md) page
+
## Pagination
Sometimes the returned result will span across many pages. When listing
@@ -233,7 +242,7 @@ resources you can pass the following parameters:
In the example below, we list 50 [namespaces](namespaces.md) per page.
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/namespaces?per_page=50
```
### Pagination Link header
@@ -247,7 +256,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue
with ID `8` which belongs to the project with ID `8`:
```bash
-curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
+curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/issues/8/notes?per_page=3&page=2
```
The response will then be:
@@ -258,7 +267,7 @@ Cache-Control: no-cache
Content-Length: 1103
Content-Type: application/json
Date: Mon, 18 Jan 2016 09:43:18 GMT
-Link: <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
+Link: <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
Status: 200 OK
Vary: Origin
X-Next-Page: 3
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index dee3e384080..96b8d654c58 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -28,8 +28,8 @@ GET /projects/:id/access_requests
| `id` | integer/string | yes | The group/project ID or path |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests
```
Example response:
@@ -69,8 +69,8 @@ POST /projects/:id/access_requests
| `id` | integer/string | yes | The group/project ID or path |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests
```
Example response:
@@ -102,8 +102,8 @@ PUT /projects/:id/access_requests/:user_id/approve
| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id/approve?access_level=20
```
Example response:
@@ -134,6 +134,6 @@ DELETE /projects/:id/access_requests/:user_id
| `user_id` | integer | yes | The user ID of the access requester |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id
```
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 58092bdd400..f57928d3c93 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -14,20 +14,20 @@ requests, snippets, and notes/comments. Issues, merge requests, snippets, and no
Gets a list of all award emoji
```
-GET /projects/:id/issues/:issue_id/award_emoji
-GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+GET /projects/:id/issues/:issue_iid/award_emoji
+GET /projects/:id/merge_requests/:merge_request_iid/award_emoji
GET /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji
```
Example Response:
@@ -74,21 +74,21 @@ Example Response:
Gets a single award emoji from an issue, snippet, or merge request.
```
-GET /projects/:id/issues/:issue_id/award_emoji/:award_id
-GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+GET /projects/:id/issues/:issue_iid/award_emoji/:award_id
+GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
-| `award_id` | integer | yes | The ID of the award emoji |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
+| `award_id` | integer | yes | The ID of the award emoji |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1
```
Example Response:
@@ -117,21 +117,21 @@ Example Response:
This end point creates an award emoji on the specified resource
```
-POST /projects/:id/issues/:issue_id/award_emoji
-POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+POST /projects/:id/issues/:issue_iid/award_emoji
+POST /projects/:id/merge_requests/:merge_request_iid/award_emoji
POST /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `awardable_id` | integer | yes | The ID of an awardable |
-| `name` | string | yes | The name of the emoji, without colons |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable |
+| `name` | string | yes | The name of the emoji, without colons |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish
```
Example Response:
@@ -161,42 +161,21 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a
admins or the author of the award.
```
-DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
-DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id
+DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `award_id` | integer | yes | The ID of a award_emoji |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of an issue |
+| `award_id` | integer | yes | The ID of a award_emoji |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
-```
-
-Example Response:
-
-```json
-{
- "id": 344,
- "name": "blowfish",
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/root"
- },
- "created_at": "2016-06-17T17:47:29.266Z",
- "updated_at": "2016-06-17T17:47:29.266Z",
- "awardable_id": 80,
- "awardable_type": "Issue"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344
```
## Award Emoji on Notes
@@ -209,20 +188,20 @@ easily adapted for notes on a Merge Request.
### List a note's award emoji
```
-GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of an note |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of an issue |
+| `note_id` | integer | yes | The ID of an note |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji
```
Example Response:
@@ -251,20 +230,20 @@ Example Response:
### Get single note's award emoji
```
-GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `award_id` | integer | yes | The ID of the award emoji |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of the award emoji |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2
```
Example Response:
@@ -291,20 +270,20 @@ Example Response:
### Award a new emoji on a note
```
-POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `name` | string | yes | The name of the emoji, without colons |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `name` | string | yes | The name of the emoji, without colons |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket
```
Example Response:
@@ -334,41 +313,20 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a
admins or the author of the award.
```
-DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
-| `note_id` | integer | yes | The ID of a note |
-| `award_id` | integer | yes | The ID of a award_emoji |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of a award_emoji |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
-```
-
-Example Response:
-
-```json
-{
- "id": 345,
- "name": "rocket",
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/root"
- },
- "created_at": "2016-06-17T19:59:55.888Z",
- "updated_at": "2016-06-17T19:59:55.888Z",
- "awardable_id": 1,
- "awardable_type": "Note"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345
```
[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575
diff --git a/doc/api/boards.md b/doc/api/boards.md
index c83db6df80c..a74e82335eb 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -18,7 +18,7 @@ GET /projects/:id/boards
| `id` | integer | yes | The ID of a project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
```
Example response:
@@ -75,7 +75,7 @@ GET /projects/:id/boards/:board_id/lists
| `board_id` | integer | yes | The ID of a board |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
```
Example response:
@@ -127,7 +127,7 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| `list_id`| integer | yes | The ID of a board's list |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
Example response:
@@ -159,7 +159,7 @@ POST /projects/:id/boards/:board_id/lists
| `label_id` | integer | yes | The ID of a label |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
```
Example response:
@@ -192,7 +192,7 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| `position` | integer | yes | The position of the list |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
```
Example response:
@@ -224,18 +224,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| `list_id` | integer | yes | The ID of a board's list |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
-```
-Example response:
-
-```json
-{
- "id" : 1,
- "label" : {
- "name" : "Testing",
- "color" : "#F0AD4E",
- "description" : null
- },
- "position" : 1
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 765ca439720..815aabda8e3 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -3,6 +3,8 @@
## List repository branches
Get a list of repository branches from a project, sorted by name alphabetically.
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
```
GET /projects/:id/repository/branches
@@ -13,7 +15,7 @@ GET /projects/:id/repository/branches
| `id` | integer | yes | The ID of a project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches
```
Example response:
@@ -48,7 +50,8 @@ Example response:
## Get single repository branch
-Get a single project repository branch.
+Get a single project repository branch. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/branches/:branch
@@ -60,7 +63,7 @@ GET /projects/:id/repository/branches/:branch
| `branch` | string | yes | The name of the branch |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master
```
Example response:
@@ -101,7 +104,7 @@ PUT /projects/:id/repository/branches/:branch/protect
```
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
```
| Attribute | Type | Required | Description |
@@ -149,7 +152,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect
```
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect
```
| Attribute | Type | Required | Description |
@@ -197,7 +200,7 @@ POST /projects/:id/repository/branches
| `ref` | string | yes | The branch name or commit SHA to create branch from |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch=newbranch&ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master"
```
Example response:
@@ -241,15 +244,7 @@ DELETE /projects/:id/repository/branches/:branch
In case of an error, an explaining message is provided.
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
-```
-
-Example response:
-
-```json
-{
- "branch_name": "newbranch"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch"
```
## Delete merged branches
@@ -266,5 +261,5 @@ DELETE /projects/:id/repository/merged_branches
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches"
```
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index a3e9c01f335..ad254e3515e 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -13,7 +13,7 @@ GET /broadcast_messages
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages
```
Example response:
@@ -43,7 +43,7 @@ GET /broadcast_messages/:id
| `id` | integer | yes | Broadcast message ID |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
```
Example response:
@@ -75,7 +75,7 @@ POST /broadcast_messages
| `font` | string | no | Foreground color hex code |
```bash
-curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages
```
Example response:
@@ -108,7 +108,7 @@ PUT /broadcast_messages/:id
| `font` | string | no | Foreground color hex code |
```bash
-curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
```
Example response:
@@ -136,19 +136,5 @@ DELETE /broadcast_messages/:id
| `id` | integer | yes | Broadcast message ID |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
-```
-
-Example response:
-
-```json
-{
- "message":"Update message",
- "starts_at":"2016-08-26T00:41:35.060Z",
- "ends_at":"2016-08-26T01:41:35.060Z",
- "color":"#000",
- "font":"#FFFFFF",
- "id":1,
- "active": true
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1
```
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index b6459971420..20d924ab35e 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -1,118 +1 @@
-# Build triggers
-
-You can read more about [triggering builds through the API](../ci/triggers/README.md).
-
-## List project triggers
-
-Get a list of project's build triggers.
-
-```
-GET /projects/:id/triggers
-```
-
-| Attribute | Type | required | Description |
-|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
-```
-
-```json
-[
- {
- "created_at": "2015-12-23T16:24:34.716Z",
- "deleted_at": null,
- "last_used": "2016-01-04T15:41:21.986Z",
- "token": "fbdb730c2fbdb095a0862dbd8ab88b",
- "updated_at": "2015-12-23T16:24:34.716Z"
- },
- {
- "created_at": "2015-12-23T16:25:56.760Z",
- "deleted_at": null,
- "last_used": null,
- "token": "7b9148c158980bbd9bcea92c17522d",
- "updated_at": "2015-12-23T16:25:56.760Z"
- }
-]
-```
-
-## Get trigger details
-
-Get details of project's build trigger.
-
-```
-GET /projects/:id/triggers/:token
-```
-
-| Attribute | Type | required | Description |
-|-----------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
-| `token` | string | yes | The `token` of a trigger |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
-
-```json
-{
- "created_at": "2015-12-23T16:25:56.760Z",
- "deleted_at": null,
- "last_used": null,
- "token": "7b9148c158980bbd9bcea92c17522d",
- "updated_at": "2015-12-23T16:25:56.760Z"
-}
-```
-
-## Create a project trigger
-
-Create a build trigger for a project.
-
-```
-POST /projects/:id/triggers
-```
-
-| Attribute | Type | required | Description |
-|-----------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
-```
-
-```json
-{
- "created_at": "2016-01-07T09:53:58.235Z",
- "deleted_at": null,
- "last_used": null,
- "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
- "updated_at": "2016-01-07T09:53:58.235Z"
-}
-```
-
-## Remove a project trigger
-
-Remove a project's build trigger.
-
-```
-DELETE /projects/:id/triggers/:token
-```
-
-| Attribute | Type | required | Description |
-|-----------|---------|----------|--------------------------|
-| `id` | integer | yes | The ID of a project |
-| `token` | string | yes | The `token` of a trigger |
-
-```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
-```
-
-```json
-{
- "created_at": "2015-12-23T16:25:56.760Z",
- "deleted_at": "2015-12-24T12:32:20.100Z",
- "last_used": null,
- "token": "7b9148c158980bbd9bcea92c17522d",
- "updated_at": "2015-12-24T12:32:20.100Z"
-}
-```
+This document was moved to [Pipeline Triggers](pipeline_triggers.md).
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 917e9773913..1c26e9b33ab 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -13,7 +13,7 @@ GET /projects/:id/variables
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables"
```
```json
@@ -43,7 +43,7 @@ GET /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/TEST_VARIABLE_1"
```
```json
@@ -68,7 +68,7 @@ POST /projects/:id/variables
| `value` | string | yes | The `value` of a variable |
```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
```
```json
@@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key
| `value` | string | yes | The `value` of a variable |
```
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
```
```json
@@ -117,12 +117,5 @@ DELETE /projects/:id/variables/:key
| `key` | string | yes | The `key` of a variable |
```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
-```
-
-```json
-{
- "key": "VARIABLE_1",
- "value": "VALUE_1"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/VARIABLE_1"
```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index bca2f9e44ef..a6edda68bc4 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -1,610 +1 @@
-# Builds API
-
-## List project builds
-
-Get a list of builds in a project.
-
-```
-GET /projects/:id/builds
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
-```
-
-Example of response
-
-```json
-[
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.802Z",
- "artifacts_file": {
- "filename": "artifacts.zip",
- "size": 1000
- },
- "finished_at": "2015-12-24T17:54:27.895Z",
- "id": 7,
- "name": "teaspoon",
- "pipeline": {
- "id": 6,
- "ref": "master",
- "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "status": "pending"
- },
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:27.722Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/root",
- "website_url": ""
- }
- },
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:24.921Z",
- "id": 6,
- "name": "spinach:other",
- "pipeline": {
- "id": 6,
- "ref": "master",
- "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "status": "pending"
- },
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:24.729Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/root",
- "website_url": ""
- }
- }
-]
-```
-
-## List commit builds
-
-Get a list of builds for specific commit in a project.
-
-This endpoint will return all builds, from all pipelines for a given commit.
-If the commit SHA is not found, it will respond with 404, otherwise it will
-return an array of builds (an empty array if there are no builds for this
-particular commit).
-
-```
-GET /projects/:id/repository/commits/:sha/builds
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `sha` | string | yes | The SHA id of a commit |
-| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running'
-```
-
-Example of response
-
-```json
-[
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": "2016-01-11T10:14:09.526Z",
- "id": 69,
- "name": "rubocop",
- "pipeline": {
- "id": 6,
- "ref": "master",
- "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "status": "pending"
- },
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "canceled",
- "tag": false,
- "user": null
- },
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.957Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:33.913Z",
- "id": 9,
- "name": "brakeman",
- "pipeline": {
- "id": 6,
- "ref": "master",
- "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "status": "pending"
- },
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:33.727Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/root",
- "website_url": ""
- }
- }
-]
-```
-
-## Get a single build
-
-Get a single build of a project
-
-```
-GET /projects/:id/builds/:build_id
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
-```
-
-Example of response
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.880Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:31.198Z",
- "id": 8,
- "name": "rubocop",
- "pipeline": {
- "id": 6,
- "ref": "master",
- "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "status": "pending"
- },
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:30.733Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/root",
- "website_url": ""
- }
-}
-```
-
-## Get build artifacts
-
-> [Introduced][ce-2893] in GitLab 8.5
-
-Get build artifacts of a project
-
-```
-GET /projects/:id/builds/:build_id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
-```
-
-Response:
-
-| Status | Description |
-|-----------|---------------------------------|
-| 200 | Serves the artifacts file |
-| 404 | Build not found or no artifacts |
-
-[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
-
-## Download the artifacts file
-
-> [Introduced][ce-5347] in GitLab 8.10.
-
-Download the artifacts file from the given reference name and job provided the
-build finished successfully.
-
-```
-GET /projects/:id/builds/artifacts/:ref_name/download?job=name
-```
-
-Parameters
-
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|-------------------------- |
-| `id` | integer | yes | The ID of a project |
-| `ref_name` | string | yes | The ref from a repository |
-| `job` | string | yes | The name of the job |
-
-Example request:
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test"
-```
-
-Example response:
-
-| Status | Description |
-|-----------|---------------------------------|
-| 200 | Serves the artifacts file |
-| 404 | Build not found or no artifacts |
-
-[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
-
-## Get a trace file
-
-Get a trace of a specific build of a project
-
-```
-GET /projects/:id/builds/:build_id/trace
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| id | integer | yes | The ID of a project |
-| build_id | integer | yes | The ID of a build |
-
-```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
-```
-
-Response:
-
-| Status | Description |
-|-----------|-----------------------------------|
-| 200 | Serves the trace file |
-| 404 | Build not found or no trace file |
-
-## Cancel a build
-
-Cancel a single build of a project
-
-```
-POST /projects/:id/builds/:build_id/cancel
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
-```
-
-Example of response
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": "2016-01-11T10:14:09.526Z",
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "canceled",
- "tag": false,
- "user": null
-}
-```
-
-## Retry a build
-
-Retry a single build of a project
-
-```
-POST /projects/:id/builds/:build_id/retry
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
-```
-
-Example of response
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "pending",
- "tag": false,
- "user": null
-}
-```
-
-## Erase a build
-
-Erase a single build of a project (remove build artifacts and a build trace)
-
-```
-POST /projects/:id/builds/:build_id/erase
-```
-
-Parameters
-
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-Example of request
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
-```
-
-Example of response
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "download_url": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "created_at": "2016-01-11T10:13:33.506Z",
- "started_at": "2016-01-11T10:13:33.506Z",
- "finished_at": "2016-01-11T10:15:10.506Z",
- "status": "failed",
- "tag": false,
- "user": null
-}
-```
-
-## Keep artifacts
-
-Prevents artifacts from being deleted when expiration is set.
-
-```
-POST /projects/:id/builds/:build_id/artifacts/keep
-```
-
-Parameters
-
-| Attribute | Type | Required | Description |
-|-------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-Example request:
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
-```
-
-Example response:
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "download_url": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "created_at": "2016-01-11T10:13:33.506Z",
- "started_at": "2016-01-11T10:13:33.506Z",
- "finished_at": "2016-01-11T10:15:10.506Z",
- "status": "failed",
- "tag": false,
- "user": null
-}
-```
-
-## Play a build
-
-Triggers a manual action to start a build.
-
-```
-POST /projects/:id/builds/:build_id/play
-```
-
-| Attribute | Type | Required | Description |
-|------------|---------|----------|---------------------|
-| `id` | integer | yes | The ID of a project |
-| `build_id` | integer | yes | The ID of a build |
-
-```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
-```
-
-Example of response
-
-```json
-{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "started",
- "tag": false,
- "user": null
-}
-```
+This document was moved to [another location](jobs.md).
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
index b6d79706a84..c8374d94716 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -5,7 +5,7 @@ API used by runners to receive and update builds.
>**Note:**
This API is intended to be used only by Runners as their own
communication channel. For the consumer API see the
-[Builds API](../builds.md).
+[Jobs API](../jobs.md).
## Authentication
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
index 0c96b3ee335..74def207816 100644
--- a/doc/api/ci/lint.md
+++ b/doc/api/ci/lint.md
@@ -13,7 +13,7 @@ POST ci/lint
| `content` | string | yes | the .gitlab-ci.yaml content|
```bash
-curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
+curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
```
Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 18bc2873678..24c402346b1 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -16,7 +16,7 @@ GET /projects/:id/repository/commits
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
```
Example response:
@@ -114,7 +114,7 @@ PAYLOAD=$(cat << 'JSON'
}
JSON
)
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v4/projects/1/repository/commits
```
Example response:
@@ -159,7 +159,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master
```
Example response:
@@ -208,7 +208,7 @@ Parameters:
| `branch` | string | yes | The name of the branch |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/cherry_pick"
```
Example response:
@@ -249,7 +249,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/diff"
```
Example response:
@@ -285,7 +285,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/comments"
```
Example response:
@@ -338,7 +338,7 @@ POST /projects/:id/repository/commits/:sha/comments
| `line_type` | string | no | The line type. Takes `new` or `old` as arguments |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
```
Example response:
@@ -383,7 +383,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| `all` | boolean | no | Return all statuses, not only the latest ones
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
```
Example response:
@@ -459,7 +459,7 @@ POST /projects/:id/statuses/:sha
| `coverage` | float | no | The total code coverage
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
```
Example response:
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index 73cb4b7ea8c..f94dbfa4059 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -7,16 +7,16 @@ First, find the ID of the projects you're interested in, by either listing all
projects:
```
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/projects
```
Or finding the ID of a group and then listing all projects in that group:
```
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups
# For group 1234:
-curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups/1234
```
With those IDs, add the same deploy key to all:
@@ -24,6 +24,6 @@ With those IDs, add the same deploy key to all:
```
for project_id in 321 456 987; do
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \
- --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys
+ --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v4/projects/${project_id}/deploy_keys
done
```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 39afc4b2df5..f051f55ac3e 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -9,7 +9,7 @@ GET /deploy_keys
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/deploy_keys"
```
Example response:
@@ -46,7 +46,7 @@ GET /projects/:id/deploy_keys
| `id` | integer | yes | The ID of the project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys"
```
Example response:
@@ -86,7 +86,7 @@ Parameters:
| `key_id` | integer | yes | The ID of the deploy key |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/11"
```
Example response:
@@ -120,7 +120,7 @@ POST /projects/:id/deploy_keys
| `can_push` | boolean | no | Can deploy key push to the project's repository |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v4/projects/5/deploy_keys/"
```
Example response:
@@ -149,19 +149,7 @@ DELETE /projects/:id/deploy_keys/:key_id
| `key_id` | integer | yes | The ID of the deploy key |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
-```
-
-Example response:
-
-```json
-{
- "id": 6,
- "deploy_key_id": 14,
- "project_id": 1,
- "created_at" : "2015-08-29T12:50:57.259Z",
- "updated_at" : "2015-08-29T12:50:57.259Z"
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/13"
```
## Enable a deploy key
@@ -169,7 +157,7 @@ Example response:
Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful.
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/deploy_keys/13/enable
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 3d95c4cde60..76e18c8a9bd 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -13,7 +13,7 @@ GET /projects/:id/deployments
| `id` | integer | yes | The ID of a project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments"
```
Example of response
@@ -151,7 +151,7 @@ GET /projects/:id/deployments/:deployment_id
| `deployment_id` | integer | yes | The ID of the deployment |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments/1"
```
Example of response
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index e0ee20d9610..3f0a8d989f9 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -13,7 +13,7 @@ GET /projects/:id/environments
| `id` | integer | yes | The ID of the project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/environments
```
Example response:
@@ -33,7 +33,7 @@ Example response:
Creates a new environment with the given name and external_url.
-It returns 201 if the environment was successfully created, 400 for wrong parameters.
+It returns `201` if the environment was successfully created, `400` for wrong parameters.
```
POST /projects/:id/environment
@@ -46,7 +46,7 @@ POST /projects/:id/environment
| `external_url` | string | no | Place to link to for this environment |
```bash
-curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments"
+curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments"
```
Example response:
@@ -64,7 +64,7 @@ Example response:
Updates an existing environment's name and/or external_url.
-It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned.
+It returns `200` if the environment was successfully updated. In case of an error, a status code `400` is returned.
```
PUT /projects/:id/environments/:environments_id
@@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id
| `external_url` | string | no | The new external_url |
```bash
-curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
+curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1"
```
Example response:
@@ -94,7 +94,7 @@ Example response:
## Delete an environment
-It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist.
+It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist.
```
DELETE /projects/:id/environments/:environment_id
@@ -106,7 +106,24 @@ DELETE /projects/:id/environments/:environment_id
| `environment_id` | integer | yes | The ID of the environment |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1"
+```
+
+## Stop an environment
+
+It returns `200` if the environment was successfully stopped, and `404` if the environment does not exist.
+
+```
+POST /projects/:id/environments/:environment_id/stop
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
+| `environment_id` | integer | yes | The ID of the environment |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop"
```
Example response:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 4a39dbc5555..dfc6b80bfd9 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -14,6 +14,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
+| `owned` | boolean | no | Limit by groups owned by the current user |
```
GET /groups
@@ -26,7 +27,7 @@ GET /groups
"name": "Foobar Group",
"path": "foo-bar",
"description": "An interesting group",
- "visibility_level": 20,
+ "visibility": "public",
"lfs_enabled": true,
"avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
"web_url": "http://localhost:3000/groups/foo-bar",
@@ -40,20 +41,6 @@ GET /groups
You can search for groups by name or path, see below.
-## List owned groups
-
-Get a list of groups which are owned by the authenticated user.
-
-```
-GET /groups/owned
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `statistics` | boolean | no | Include group statistics |
-
## List a group's projects
Get a list of projects in this group.
@@ -85,9 +72,8 @@ Example response:
"description": "foo",
"default_branch": "master",
"tag_list": [],
- "public": false,
"archived": false,
- "visibility_level": 10,
+ "visibility": "internal",
"ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
"http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
"web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
@@ -98,7 +84,7 @@ Example response:
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"snippets_enabled": true,
"created_at": "2016-04-05T21:40:50.169Z",
"last_activity_at": "2016-04-06T16:52:08.432Z",
@@ -114,7 +100,7 @@ Example response:
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
}
@@ -136,7 +122,7 @@ Parameters:
| `id` | integer/string | yes | The ID or path of a group |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
```
Example response:
@@ -147,7 +133,7 @@ Example response:
"name": "Twitter",
"path": "twitter",
"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "visibility_level": 20,
+ "visibility": "public",
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false,
@@ -160,9 +146,8 @@ Example response:
"description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
"default_branch": "master",
"tag_list": [],
- "public": true,
"archived": false,
- "visibility_level": 20,
+ "visibility": "public",
"ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
"http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
"web_url": "https://gitlab.example.com/twitter/typeahead-js",
@@ -173,7 +158,7 @@ Example response:
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:25.578Z",
@@ -190,7 +175,7 @@ Example response:
"star_count": 0,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
},
@@ -199,9 +184,8 @@ Example response:
"description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
"default_branch": "master",
"tag_list": [],
- "public": false,
"archived": false,
- "visibility_level": 10,
+ "visibility": "internal",
"ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
"http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
"web_url": "https://gitlab.example.com/twitter/flight",
@@ -212,7 +196,7 @@ Example response:
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:24.661Z",
@@ -229,7 +213,7 @@ Example response:
"star_count": 0,
"forks_count": 0,
"open_issues_count": 8,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
}
@@ -240,9 +224,8 @@ Example response:
"description": "Velit eveniet provident fugiat saepe eligendi autem.",
"default_branch": "master",
"tag_list": [],
- "public": false,
"archived": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
"http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
"web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
@@ -253,7 +236,7 @@ Example response:
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:27.089Z",
@@ -270,7 +253,7 @@ Example response:
"star_count": 0,
"forks_count": 0,
"open_issues_count": 4,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [
{
"group_id": 4,
@@ -301,7 +284,7 @@ Parameters:
- `name` (required) - The name of the group
- `path` (required) - The path of the group
- `description` (optional) - The group's description
-- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+- `visibility` (optional) - The group's visibility. Can be `private`, `internal`, or `public`.
- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
- `request_access_enabled` (optional) - Allow users to request member access.
- `parent_id` (optional) - The parent group id for creating nested group.
@@ -333,12 +316,12 @@ PUT /groups/:id
| `name` | string | no | The name of the group |
| `path` | string | no | The path of the group |
| `description` | string | no | The description of the group |
-| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
```
@@ -350,7 +333,7 @@ Example response:
"name": "Experimental",
"path": "h5bp",
"description": "foo",
- "visibility_level": 10,
+ "visibility": "internal",
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
"request_access_enabled": false,
@@ -365,7 +348,7 @@ Example response:
"tag_list": [],
"public": false,
"archived": false,
- "visibility_level": 10,
+ "visibility": "internal",
"ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
"http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
"web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
@@ -376,7 +359,7 @@ Example response:
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"snippets_enabled": true,
"created_at": "2016-04-05T21:40:50.169Z",
"last_activity_at": "2016-04-06T16:52:08.432Z",
@@ -392,7 +375,7 @@ Example response:
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
"request_access_enabled": false
}
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 6cd701215e9..a19c965a8c3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -25,18 +25,20 @@ GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
GET /issues?milestone=1.0.0
GET /issues?milestone=1.0.0&state=opened
+GET /issues?iids[]=42&iids[]=43
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string| no | The milestone title |
+| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues
```
Example response:
@@ -80,7 +82,6 @@ Example response:
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
- "subscribed" : false,
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/6",
@@ -102,20 +103,22 @@ GET /groups/:id/issues?labels=foo,bar
GET /groups/:id/issues?labels=foo,bar&state=opened
GET /groups/:id/issues?milestone=1.0.0
GET /groups/:id/issues?milestone=1.0.0&state=opened
+GET /groups/:id/issues?iids[]=42&iids[]=43
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a group |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
+| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4/issues
```
Example response:
@@ -159,7 +162,6 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
- "subscribed" : false,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
@@ -181,21 +183,22 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
+GET /projects/:id/issues?iids[]=42&iids[]=43
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `iid` | integer | no | Return the issue having the given `iid` |
+| `iids` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues
```
Example response:
@@ -239,7 +242,6 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
- "subscribed" : false,
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
@@ -253,16 +255,16 @@ Example response:
Get a single project issue.
```
-GET /projects/:id/issues/:issue_id
+GET /projects/:id/issues/:issue_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id`| integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/41
```
Example response:
@@ -321,21 +323,22 @@ Creates a new project issue.
POST /projects/:id/issues
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `title` | string | yes | The title of an issue |
-| `description` | string | no | The description of an issue |
-| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
-| `assignee_id` | integer | no | The ID of a user to assign issue |
-| `milestone_id` | integer | no | The ID of a milestone to assign issue |
-| `labels` | string | no | Comma-separated label names for an issue |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
-| `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `title` | string | yes | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
+| `assignee_id` | integer | no | The ID of a user to assign issue |
+| `milestone_id` | integer | no | The ID of a milestone to assign issue |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
+| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
```
Example response:
@@ -377,25 +380,25 @@ Updates an existing project issue. This call is also used to mark an issue as
closed.
```
-PUT /projects/:id/issues/:issue_id
+PUT /projects/:id/issues/:issue_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `title` | string | no | The title of an issue |
-| `description` | string | no | The description of an issue |
-| `confidential` | boolean | no | Updates an issue to be confidential |
-| `assignee_id` | integer | no | The ID of a user to assign the issue to |
-| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
-| `labels` | string | no | Comma-separated label names for an issue |
-| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
-| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `title` | string | no | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Updates an issue to be confidential |
+| `assignee_id` | integer | no | The ID of a user to assign the issue to |
+| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
+| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
```
Example response:
@@ -436,16 +439,16 @@ Example response:
Only for admins and project owners. Soft deletes the issue in question.
```
-DELETE /projects/:id/issues/:issue_id
+DELETE /projects/:id/issues/:issue_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85
```
## Move an issue
@@ -458,17 +461,17 @@ If a given label and/or milestone with the same name also exists in the target
project, it will then be assigned to the issue that is being moved.
```
-POST /projects/:id/issues/:issue_id/move
+POST /projects/:id/issues/:issue_iid/move
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `to_project_id` | integer | yes | The ID of the new project |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `to_project_id` | integer | yes | The ID of the new project |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move
```
Example response:
@@ -514,16 +517,16 @@ If the user is already subscribed to the issue, the status code `304`
is returned.
```
-POST /projects/:id/issues/:issue_id/subscribe
+POST /projects/:id/issues/:issue_iid/subscribe
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe
```
Example response:
@@ -569,53 +572,16 @@ from it. If the user is not subscribed to the issue, the
status code `304` is returned.
```
-DELETE /projects/:id/issues/:issue_id/unsubscribe
+POST /projects/:id/issues/:issue_iid/unsubscribe
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe
-```
-
-Example response:
-
-```json
-{
- "id": 93,
- "iid": 12,
- "project_id": 5,
- "title": "Incidunt et rerum ea expedita iure quibusdam.",
- "description": "Et cumque architecto sed aut ipsam.",
- "state": "opened",
- "created_at": "2016-04-05T21:41:45.217Z",
- "updated_at": "2016-04-07T13:02:37.905Z",
- "labels": [],
- "milestone": null,
- "assignee": {
- "name": "Edwardo Grady",
- "username": "keyon",
- "id": 21,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/keyon"
- },
- "author": {
- "name": "Vivian Hermann",
- "username": "orville",
- "id": 11,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/orville"
- },
- "subscribed": false,
- "due_date": null,
- "web_url": "http://example.com/example/example/issues/12",
- "confidential": false
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
```
## Create a todo
@@ -625,16 +591,16 @@ there already exists a todo for the user on that issue, status code `304` is
returned.
```
-POST /projects/:id/issues/:issue_id/todo
+POST /projects/:id/issues/:issue_iid/todo
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/todo
```
Example response:
@@ -716,17 +682,17 @@ Example response:
Sets an estimated time of work for this issue.
```
-POST /projects/:id/issues/:issue_id/time_estimate
+POST /projects/:id/issues/:issue_iid/time_estimate
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_estimate?duration=3h30m
```
Example response:
@@ -745,16 +711,16 @@ Example response:
Resets the estimated time for this issue to 0 seconds.
```
-POST /projects/:id/issues/:issue_id/reset_time_estimate
+POST /projects/:id/issues/:issue_iid/reset_time_estimate
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_time_estimate
```
Example response:
@@ -773,17 +739,17 @@ Example response:
Adds spent time for this issue
```
-POST /projects/:id/issues/:issue_id/add_spent_time
+POST /projects/:id/issues/:issue_iid/add_spent_time
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/add_spent_time?duration=1h
```
Example response:
@@ -802,16 +768,16 @@ Example response:
Resets the total spent time for this issue to 0 seconds.
```
-POST /projects/:id/issues/:issue_id/reset_spent_time
+POST /projects/:id/issues/:issue_iid/reset_spent_time
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_spent_time
```
Example response:
@@ -828,16 +794,16 @@ Example response:
## Get time tracking stats
```
-GET /projects/:id/issues/:issue_id/time_stats
+GET /projects/:id/issues/:issue_iid/time_stats
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of a project's issue |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_stats
```
Example response:
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
new file mode 100644
index 00000000000..7340123e09d
--- /dev/null
+++ b/doc/api/jobs.md
@@ -0,0 +1,622 @@
+# Jobs API
+
+## List project jobs
+
+Get a list of jobs in a project.
+
+```
+GET /projects/:id/jobs
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
+```
+
+Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## List pipeline jobs
+
+Get a list of jobs for a pipeline.
+
+```
+GET /projects/:id/pipeline/:pipeline_id/jobs
+```
+
+| Attribute | Type | Required | Description |
+|---------------|--------------------------------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running'
+```
+
+Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## Get a single job
+
+Get a single job of a project
+
+```
+GET /projects/:id/jobs/:job_id
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "id": 8,
+ "name": "rubocop",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+}
+```
+
+## Get job artifacts
+
+> [Introduced][ce-2893] in GitLab 8.5
+
+Get job artifacts of a project
+
+```
+GET /projects/:id/jobs/:job_id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Serves the artifacts file |
+| 404 | Build not found or no artifacts |
+
+[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
+
+## Download the artifacts file
+
+> [Introduced][ce-5347] in GitLab 8.10.
+
+Download the artifacts file from the given reference name and job provided the
+job finished successfully.
+
+```
+GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
+```
+
+Parameters
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|-------------------------- |
+| `id` | integer | yes | The ID of a project |
+| `ref_name` | string | yes | The ref from a repository |
+| `job` | string | yes | The name of the job |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
+```
+
+Example response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Serves the artifacts file |
+| 404 | Build not found or no artifacts |
+
+[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
+
+## Get a trace file
+
+Get a trace of a specific job of a project
+
+```
+GET /projects/:id/jobs/:job_id/trace
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| job_id | integer | yes | The ID of a job |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace"
+```
+
+Response:
+
+| Status | Description |
+|-----------|-----------------------------------|
+| 200 | Serves the trace file |
+| 404 | Build not found or no trace file |
+
+## Cancel a job
+
+Cancel a single job of a project
+
+```
+POST /projects/:id/jobs/:job_id/cancel
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/cancel"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+}
+```
+
+## Retry a job
+
+Retry a single job of a project
+
+```
+POST /projects/:id/jobs/:job_id/retry
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/retry"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "pending",
+ "tag": false,
+ "user": null
+}
+```
+
+## Erase a job
+
+Erase a single job of a project (remove job artifacts and a job trace)
+
+```
+POST /projects/:id/jobs/:job_id/erase
+```
+
+Parameters
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+Example of request
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/erase"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "download_url": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "started_at": "2016-01-11T10:13:33.506Z",
+ "finished_at": "2016-01-11T10:15:10.506Z",
+ "status": "failed",
+ "tag": false,
+ "user": null
+}
+```
+
+## Keep artifacts
+
+Prevents artifacts from being deleted when expiration is set.
+
+```
+POST /projects/:id/jobs/:job_id/artifacts/keep
+```
+
+Parameters
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+Example request:
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts/keep"
+```
+
+Example response:
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "download_url": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "started_at": "2016-01-11T10:13:33.506Z",
+ "finished_at": "2016-01-11T10:15:10.506Z",
+ "status": "failed",
+ "tag": false,
+ "user": null
+}
+```
+
+## Play a job
+
+Triggers a manual action to start a job.
+
+```
+POST /projects/:id/jobs/:job_id/play
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `job_id` | integer | yes | The ID of a job |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/1/play"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "started",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/api/labels.md b/doc/api/labels.md
index a1e7eb1a7b1..e8c220f6809 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -13,7 +13,7 @@ GET /projects/:id/labels
| `id` | integer | yes | The ID of the project |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/labels
```
Example response:
@@ -95,7 +95,7 @@ POST /projects/:id/labels
| `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
```bash
-curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels"
```
Example response:
@@ -128,23 +128,7 @@ DELETE /projects/:id/labels
| `name` | string | yes | The name of the label |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
-```
-
-Example response:
-
-```json
-{
- "id" : 1,
- "name" : "bug",
- "color" : "#d9534f",
- "description": "Bug reported by user",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels?name=bug"
```
## Edit an existing label
@@ -167,7 +151,7 @@ PUT /projects/:id/labels
```bash
-curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels"
```
Example response:
@@ -202,7 +186,7 @@ POST /projects/:id/labels/:label_id/subscribe
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/subscribe
```
Example response:
@@ -228,7 +212,7 @@ from it. If the user is not subscribed to the label, the
status code `304` is returned.
```
-DELETE /projects/:id/labels/:label_id/unsubscribe
+POST /projects/:id/labels/:label_id/unsubscribe
```
| Attribute | Type | Required | Description |
@@ -237,21 +221,5 @@ DELETE /projects/:id/labels/:label_id/unsubscribe
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe
-```
-
-Example response:
-
-```json
-{
- "id" : 1,
- "name" : "bug",
- "color" : "#d9534f",
- "description": "Bug reported by user",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/unsubscribe
```
diff --git a/doc/api/members.md b/doc/api/members.md
index 5dcb2a5f60a..fe46f8f84bc 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -27,8 +27,8 @@ GET /projects/:id/members
| `query` | string | no | A query string to search for members |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members
```
Example response:
@@ -69,8 +69,8 @@ GET /projects/:id/members/:user_id
| `user_id` | integer | yes | The user ID of the member |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id
```
Example response:
@@ -104,8 +104,8 @@ POST /projects/:id/members
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/groups/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/projects/:id/members
```
Example response:
@@ -138,8 +138,8 @@ PUT /projects/:id/members/:user_id
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id?access_level=40
```
Example response:
@@ -170,6 +170,6 @@ DELETE /projects/:id/members/:user_id
| `user_id` | integer | yes | The user ID of the member |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2a99ae822d7..2e0545da1c4 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -65,9 +65,8 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
- "subscribed" : false,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -83,13 +82,13 @@ Parameters:
Shows information about a single merge request.
```
-GET /projects/:id/merge_requests/:merge_request_id
+GET /projects/:id/merge_requests/:merge_request_iid
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
```json
{
@@ -134,7 +133,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -151,13 +150,13 @@ Parameters:
Get a list of merge request commits.
```
-GET /projects/:id/merge_requests/:merge_request_id/commits
+GET /projects/:id/merge_requests/:merge_request_iid/commits
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
```json
@@ -188,13 +187,13 @@ Parameters:
Shows information about the merge request including its files and changes.
```
-GET /projects/:id/merge_requests/:merge_request_id/changes
+GET /projects/:id/merge_requests/:merge_request_iid/changes
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of MR
+- `merge_request_iid` (required) - The internal ID of the merge request
```json
{
@@ -239,7 +238,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -270,18 +269,18 @@ Creates a new merge request.
POST /projects/:id/merge_requests
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | string | yes | The ID of a project |
-| `source_branch` | string | yes | The source branch |
-| `target_branch` | string | yes | The target branch |
-| `title` | string | yes | Title of MR |
-| `assignee_id` | integer | no | Assignee user ID |
-| `description` | string | no | Description of MR |
-| `target_project_id` | integer | no | The target project (numeric id) |
-| `labels` | string | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer | no | The ID of a milestone |
-| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The ID of a project |
+| `source_branch` | string | yes | The source branch |
+| `target_branch` | string | yes | The target branch |
+| `title` | string | yes | Title of MR |
+| `assignee_id` | integer | no | Assignee user ID |
+| `description` | string | no | Description of MR |
+| `target_project_id` | integer | no | The target project (numeric id) |
+| `labels` | string | no | Labels for MR as a comma-separated list |
+| `milestone_id` | integer | no | The ID of a milestone |
+| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
```json
{
@@ -326,7 +325,7 @@ POST /projects/:id/merge_requests
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -343,21 +342,21 @@ POST /projects/:id/merge_requests
Updates an existing merge request. You can change the target branch, title, or even close the MR.
```
-PUT /projects/:id/merge_requests/:merge_request_id
+PUT /projects/:id/merge_requests/:merge_request_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | string | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a merge request |
-| `target_branch` | string | no | The target branch |
-| `title` | string | no | Title of MR |
-| `assignee_id` | integer | no | Assignee user ID |
-| `description` | string | no | Description of MR |
-| `state_event` | string | no | New state (close/reopen) |
-| `labels` | string | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer | no | The ID of a milestone |
-| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The ID of a merge request |
+| `target_branch` | string | no | The target branch |
+| `title` | string | no | Title of MR |
+| `assignee_id` | integer | no | Assignee user ID |
+| `description` | string | no | Description of MR |
+| `state_event` | string | no | New state (close/reopen) |
+| `labels` | string | no | Labels for MR as a comma-separated list |
+| `milestone_id` | integer | no | The ID of a milestone |
+| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
Must include at least one non-required attribute from above.
@@ -403,7 +402,7 @@ Must include at least one non-required attribute from above.
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -420,16 +419,16 @@ Must include at least one non-required attribute from above.
Only for admins and project owners. Soft deletes the merge request in question.
```
-DELETE /projects/:id/merge_requests/:merge_request_id
+DELETE /projects/:id/merge_requests/:merge_request_iid
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/merge_requests/85
```
## Accept MR
@@ -446,16 +445,16 @@ If the `sha` parameter is passed and does not match the HEAD of the source - you
If you don't have permissions to accept this merge request - you'll get a `401`
```
-PUT /projects/:id/merge_requests/:merge_request_id/merge
+PUT /projects/:id/merge_requests/:merge_request_iid/merge
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of MR
+- `merge_request_iid` (required) - Internal ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
- `should_remove_source_branch` (optional) - if `true` removes the source branch
-- `merge_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds
+- `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds
- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail
```json
@@ -501,7 +500,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -519,14 +518,14 @@ If you don't have permissions to accept this merge request - you'll get a `401`
If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed'
-In case the merge request is not set to be merged when the build succeeds, you'll also get a `406` error.
+In case the merge request is not set to be merged when the pipeline succeeds, you'll also get a `406` error.
```
-PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds
+PUT /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of MR
+- `merge_request_iid` (required) - Internal ID of MR
```json
{
@@ -571,7 +570,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
@@ -592,16 +591,16 @@ Comments are done via the [notes](notes.md) resource.
Get all the issues that would be closed by merging the provided merge request.
```
-GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues
```
Example response when the GitLab issue tracker is used:
@@ -667,16 +666,16 @@ Subscribes the authenticated user to a merge request to receive notification. If
status code `304` is returned.
```
-POST /projects/:id/merge_requests/:merge_request_id/subscribe
+POST /projects/:id/merge_requests/:merge_request_iid/subscribe
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe
```
Example response:
@@ -726,7 +725,7 @@ Example response:
"updated_at": "2016-04-05T21:41:40.905Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"sha": "8888888888888888888888888888888888888888",
@@ -741,16 +740,16 @@ notifications from that merge request. If the user is
not subscribed to the merge request, the status code `304` is returned.
```
-DELETE /projects/:id/merge_requests/:merge_request_id/unsubscribe
+POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/unsubscribe
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe
```
Example response:
@@ -800,7 +799,7 @@ Example response:
"updated_at": "2016-04-05T21:41:40.905Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": false,
"sha": "8888888888888888888888888888888888888888",
@@ -815,16 +814,16 @@ If there already exists a todo for the user on that merge request,
status code `304` is returned.
```
-POST /projects/:id/merge_requests/:merge_request_id/todo
+POST /projects/:id/merge_requests/:merge_request_iid/todo
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo
```
Example response:
@@ -893,7 +892,7 @@ Example response:
"updated_at": "2016-06-17T07:47:33.840Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "unchecked",
"subscribed": true,
"sha": "8888888888888888888888888888888888888888",
@@ -915,16 +914,16 @@ Example response:
Get a list of merge request diff versions.
```
-GET /projects/:id/merge_requests/:merge_request_id/versions
+GET /projects/:id/merge_requests/:merge_request_iid/versions
```
-| Attribute | Type | Required | Description |
-| --------- | ------- | -------- | --------------------- |
-| `id` | String | yes | The ID of the project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions
```
Example response:
@@ -956,17 +955,17 @@ Example response:
Get a single merge request diff version.
```
-GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id
+GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id
```
-| Attribute | Type | Required | Description |
-| --------- | ------- | -------- | --------------------- |
-| `id` | String | yes | The ID of the project |
-| `merge_request_id` | integer | yes | The ID of the merge request |
-| `version_id` | integer | yes | The ID of the merge request diff version |
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
+| `version_id` | integer | yes | The ID of the merge request diff version |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions/1
```
Example response:
@@ -1023,17 +1022,17 @@ Example response:
Sets an estimated time of work for this merge request.
```
-POST /projects/:id/merge_requests/:merge_request_id/time_estimate
+POST /projects/:id/merge_requests/:merge_request_iid/time_estimate
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m
```
Example response:
@@ -1052,16 +1051,16 @@ Example response:
Resets the estimated time for this merge request to 0 seconds.
```
-POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
+POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate
```
Example response:
@@ -1080,17 +1079,17 @@ Example response:
Adds spent time for this merge request
```
-POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
+POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
-| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h
```
Example response:
@@ -1109,16 +1108,16 @@ Example response:
Resets the total spent time for this merge request to 0 seconds.
```
-POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
+POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time
```
Example response:
@@ -1135,16 +1134,16 @@ Example response:
## Get time tracking stats
```
-GET /projects/:id/merge_requests/:merge_request_id/time_stats
+GET /projects/:id/merge_requests/:merge_request_iid/time_stats
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats
```
Example response:
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index bf7dcc008e9..3c86357a6c3 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -6,10 +6,11 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
-GET /projects/:id/milestones?iid=42
-GET /projects/:id/milestones?iid[]=42&iid[]=43
+GET /projects/:id/milestones?iids=42
+GET /projects/:id/milestones?iids[]=42&iids[]=43
GET /projects/:id/milestones?state=active
GET /projects/:id/milestones?state=closed
+GET /projects/:id/milestones?search=version
```
Parameters:
@@ -17,11 +18,12 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `iid` | Array[integer] | optional | Return only the milestone having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/milestones
```
Example Response:
@@ -115,4 +117,4 @@ GET /projects/:id/milestones/:milestone_id/merge_requests
Parameters:
- `id` (required) - The ID of a project
-- `milestone_id` (required) - The ID of a project milestone \ No newline at end of file
+- `milestone_id` (required) - The ID of a project milestone
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 1d97b5de688..eef06d5f324 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -19,7 +19,7 @@ GET /namespaces
Example request:
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces
```
Example response:
@@ -60,7 +60,7 @@ GET /namespaces?search=foobar
Example request:
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces?search=twitter
```
Example response:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 214dfa4068d..6ef06b2c2e9 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -34,8 +34,6 @@ Parameters:
"created_at": "2013-10-02T09:22:45Z",
"updated_at": "2013-10-02T10:22:45Z",
"system": true,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -54,8 +52,6 @@ Parameters:
"created_at": "2013-10-02T09:56:03Z",
"updated_at": "2013-10-02T09:56:03Z",
"system": true,
- "upvote": false,
- "downvote": false,
"noteable_id": 121,
"noteable_type": "Issue"
}
@@ -124,33 +120,7 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
-```
-
-Example Response:
-
-```json
-{
- "id": 636,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-05T22:10:44.164Z",
- "system": false,
- "noteable_id": 11,
- "noteable_type": "Issue",
- "upvote": false,
- "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/notes/636
```
## Snippets
@@ -248,33 +218,7 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
-```
-
-Example Response:
-
-```json
-{
- "id": 1659,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-06T16:51:53.239Z",
- "system": false,
- "noteable_id": 52,
- "noteable_type": "Snippet",
- "upvote": false,
- "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/52/notes/1659
```
## Merge Requests
@@ -322,8 +266,6 @@ Parameters:
"created_at": "2013-10-02T08:57:14Z",
"updated_at": "2013-10-02T08:57:14Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 2,
"noteable_type": "MergeRequest"
}
@@ -377,31 +319,5 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
-```
-
-Example Response:
-
-```json
-{
- "id": 1602,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-05T22:11:59.923Z",
- "system": false,
- "noteable_id": 7,
- "noteable_type": "MergeRequest",
- "upvote": false,
- "downvote": false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602
```
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index aea1c12a392..43047917f77 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -41,7 +41,7 @@ GET /notification_settings
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings
```
Example response:
@@ -62,7 +62,7 @@ PUT /notification_settings
```
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings?level=watch
```
| Attribute | Type | Required | Description |
@@ -101,8 +101,8 @@ GET /projects/:id/notification_settings
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings
```
| Attribute | Type | Required | Description |
@@ -127,8 +127,8 @@ PUT /projects/:id/notification_settings
```
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings?level=watch
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings?level=custom&new_note=true
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings?level=watch
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings?level=custom&new_note=true
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index eab532af594..46fe64d382e 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -77,13 +77,13 @@ You can now make requests to the API with the access token returned.
The access token allows you to make requests to the API on a behalf of a user.
```
-GET https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
```
Or you can put the token to the Authorization header:
```
-curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/user
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
```
## Resource Owner Password Credentials
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
new file mode 100644
index 00000000000..fdb41a1d615
--- /dev/null
+++ b/doc/api/pipeline_triggers.md
@@ -0,0 +1,170 @@
+# Pipeline triggers
+
+You can read more about [triggering pipelines through the API](../ci/triggers/README.md).
+
+## List project triggers
+
+Get a list of project's build triggers.
+
+```
+GET /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+[
+ {
+ "id": 10,
+ "description": "my trigger",
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z",
+ "owner": null
+ }
+]
+```
+
+## Get trigger details
+
+Get details of project's build trigger.
+
+```
+GET /projects/:id/triggers/:trigger_id
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | The `token` of a trigger |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
+
+```json
+{
+ "id": 10,
+ "description": "my trigger",
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z",
+ "owner": null
+}
+```
+
+## Create a project trigger
+
+Create a trigger for a project.
+
+```
+POST /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `description` | string | yes | The trigger name |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers"
+```
+
+```json
+{
+ "id": 10,
+ "description": "my trigger",
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z",
+ "owner": null
+}
+```
+
+## Update a project trigger
+
+Update a trigger for a project.
+
+```
+PUT /projects/:id/triggers/:trigger_id
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `trigger_id` | integer | yes | The trigger id |
+| `description` | string | no | The trigger name |
+
+```
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form description="my description" "https://gitlab.example.com/api/v4/projects/1/triggers/10"
+```
+
+```json
+{
+ "id": 10,
+ "description": "my trigger",
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z",
+ "owner": null
+}
+```
+
+## Take ownership of a project trigger
+
+Update an owner of a project trigger.
+
+```
+POST /projects/:id/triggers/:trigger_id/take_ownership
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `trigger_id` | integer | yes | The trigger id |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/10/take_ownership"
+```
+
+```json
+{
+ "id": 10,
+ "description": "my trigger",
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z",
+ "owner": null
+}
+```
+
+## Remove a project trigger
+
+Remove a project's build trigger.
+
+```
+DELETE /projects/:id/triggers/:trigger_id
+```
+
+| Attribute | Type | required | Description |
+|----------------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `trigger_id` | integer | yes | The trigger id |
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
+```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index f3c9827f742..574a8bacb25 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -13,7 +13,7 @@ GET /projects/:id/pipelines
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines"
```
Example of response
@@ -24,49 +24,13 @@ Example of response
"id": 47,
"status": "pending",
"ref": "new-pipeline",
- "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
- "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
- "tag": false,
- "yaml_errors": null,
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2016-08-16T10:23:19.007Z",
- "updated_at": "2016-08-16T10:23:19.216Z",
- "started_at": null,
- "finished_at": null,
- "committed_at": null,
- "duration": null,
- "coverage": "30.0"
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a"
},
{
"id": 48,
"status": "pending",
"ref": "new-pipeline",
- "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
- "before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
- "tag": false,
- "yaml_errors": null,
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2016-08-16T10:23:21.184Z",
- "updated_at": "2016-08-16T10:23:21.314Z",
- "started_at": null,
- "finished_at": null,
- "committed_at": null,
- "duration": null,
- "coverage": null
+ "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a"
}
]
```
@@ -85,7 +49,7 @@ GET /projects/:id/pipelines/:pipeline_id
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46"
```
Example of response
@@ -131,7 +95,7 @@ POST /projects/:id/pipeline
| `ref` | string | yes | Reference to commit |
```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
```
Example of response
@@ -163,7 +127,7 @@ Example of response
}
```
-## Retry builds in a pipeline
+## Retry jobs in a pipeline
> [Introduced][ce-5837] in GitLab 8.11
@@ -177,7 +141,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry"
```
Response:
@@ -209,7 +173,7 @@ Response:
}
```
-## Cancel a pipelines builds
+## Cancel a pipelines jobs
> [Introduced][ce-5837] in GitLab 8.11
@@ -223,7 +187,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel"
```
Response:
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 404876f6237..4f6f561b83e 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -3,15 +3,15 @@
### Snippet visibility level
Snippets in GitLab can be either private, internal or public.
-You can set it with the `visibility_level` field in the snippet.
+You can set it with the `visibility` field in the snippet.
Constants for snippet visibility levels are:
-| Visibility | visibility_level | Description |
-| ---------- | ---------------- | ----------- |
-| Private | `0` | The snippet is visible only the snippet creator |
-| Internal | `10` | The snippet is visible for any logged in user |
-| Public | `20` | The snippet can be accessed without any authentication |
+| visibility | Description |
+| ---------- | ----------- |
+| `private` | The snippet is visible only the snippet creator |
+| `internal` | The snippet is visible for any logged in user |
+| `public` | The snippet can be accessed without any authentication |
## List snippets
@@ -71,7 +71,7 @@ Parameters:
- `title` (required) - The title of a snippet
- `file_name` (required) - The name of a snippet file
- `code` (required) - The content of a snippet
-- `visibility_level` (required) - The snippet's visibility
+- `visibility` (required) - The snippet's visibility
## Update snippet
@@ -88,7 +88,7 @@ Parameters:
- `title` (optional) - The title of a snippet
- `file_name` (optional) - The name of a snippet file
- `code` (optional) - The content of a snippet
-- `visibility_level` (optional) - The snippet's visibility
+- `visibility` (optional) - The snippet's visibility
## Delete snippet
diff --git a/doc/api/projects.md b/doc/api/projects.md
index e9ef03a0c0c..686f3dba35d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -4,23 +4,23 @@
### Project visibility level
Project in GitLab has be either private, internal or public.
-You can determine it by `visibility_level` field in project.
+You can determine it by `visibility` field in project.
Constants for project visibility levels are next:
-* Private. `visibility_level` is `0`.
+* `private`:
Project access must be granted explicitly for each user.
-* Internal. `visibility_level` is `10`.
+* `internal`:
The project can be cloned by any logged in user.
-* Public. `visibility_level` is `20`.
+* `public`:
The project can be cloned without any authentication.
## List projects
-Get a list of projects for which the authenticated user is a member.
+Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
```
GET /projects
@@ -34,9 +34,10 @@ Parameters:
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `search` | string | no | Return list of projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
+| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
```json
@@ -45,8 +46,7 @@ Parameters:
"id": 4,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
@@ -66,7 +66,7 @@ Parameters:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -86,9 +86,9 @@ Parameters:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
},
@@ -96,8 +96,7 @@ Parameters:
"id": 6,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
@@ -117,7 +116,7 @@ Parameters:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -147,9 +146,9 @@ Parameters:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -177,8 +176,7 @@ Parameters:
"id": 3,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -198,7 +196,7 @@ Parameters:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -228,7 +226,7 @@ Parameters:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [
{
"group_id": 4,
@@ -241,7 +239,7 @@ Parameters:
"group_access_level": 10
}
],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -407,8 +405,6 @@ Parameters:
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -437,21 +433,21 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `name` | string | yes | The name of the new project |
-| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. |
+| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | String | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -476,15 +472,15 @@ Parameters:
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -508,15 +504,15 @@ Parameters:
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
-| `builds_enabled` | boolean | no | Enable builds for this project |
+| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
+| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
-| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
-| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
+| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -551,7 +547,7 @@ Parameters:
| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/star"
```
Example response:
@@ -561,8 +557,7 @@ Example response:
"id": 3,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 10,
+ "visibility": "internal",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -577,7 +572,7 @@ Example response:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -596,9 +591,9 @@ Example response:
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 1,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -617,7 +612,7 @@ POST /projects/:id/unstar
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unstar"
```
Example response:
@@ -627,8 +622,7 @@ Example response:
"id": 3,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 10,
+ "visibility": "internal",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -643,7 +637,7 @@ Example response:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -662,9 +656,9 @@ Example response:
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -684,7 +678,7 @@ POST /projects/:id/archive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/archive"
```
Example response:
@@ -694,8 +688,7 @@ Example response:
"id": 3,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -715,7 +708,7 @@ Example response:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -745,9 +738,9 @@ Example response:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -767,7 +760,7 @@ POST /projects/:id/unarchive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unarchive"
```
Example response:
@@ -777,8 +770,7 @@ Example response:
"id": 3,
"description": null,
"default_branch": "master",
- "public": false,
- "visibility_level": 0,
+ "visibility": "private",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
@@ -798,7 +790,7 @@ Example response:
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
- "builds_enabled": true,
+ "jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": false,
@@ -828,9 +820,9 @@ Example response:
"forks_count": 0,
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
- "public_builds": true,
+ "public_jobs": true,
"shared_with_groups": [],
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false
}
@@ -916,7 +908,7 @@ Parameters:
| `group_id` | integer | yes | The ID of the group |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/share/17
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/share/17
```
## Hooks
@@ -963,7 +955,7 @@ Parameters:
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"pipeline_events": true,
"wiki_page_events": true,
"enable_ssl_verification": true,
@@ -990,7 +982,7 @@ Parameters:
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
-| `build_events` | boolean | no | Trigger hook on build events |
+| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
@@ -1016,7 +1008,7 @@ Parameters:
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
-| `build_events` | boolean | no | Trigger hook on build events |
+| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
@@ -1195,3 +1187,17 @@ Parameters:
| `query` | string | yes | A string contained in the project name |
| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
+
+## Start the Housekeeping task for a Project
+
+>**Note:** This feature was introduced in GitLab 9.0
+
+```
+POST /projects/:id/housekeeping
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 727617f1ecc..b1bf9ca07cc 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -5,6 +5,8 @@
Get a list of repository files and directories in a project. This endpoint can
be accessed without authentication if the repository is publicly accessible.
+This command provides essentially the same functionality as the `git ls-tree` command. For more information, see the section _Tree Objects_ in the [Git internals documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects).
+
```
GET /projects/:id/repository/tree
```
@@ -13,7 +15,7 @@ Parameters:
- `id` (required) - The ID of a project
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
-- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
```json
@@ -70,10 +72,11 @@ Parameters:
]
```
-## Raw file content
+## Get a blob from repository
-Get the raw file contents for a file by commit SHA and path. This endpoint can
-be accessed without authentication if the repository is publicly accessible.
+Allows you to receive information about blob in repository like size and
+content. Note that blob content is Base64 encoded. This endpoint can be accessed
+without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/blobs/:sha
@@ -83,7 +86,6 @@ Parameters:
- `id` (required) - The ID of a project
- `sha` (required) - The commit or branch name
-- `filepath` (required) - The path the file
## Raw blob content
@@ -91,7 +93,7 @@ Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
without authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/raw_blobs/:sha
+GET /projects/:id/repository/blobs/:sha/raw
```
Parameters:
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 677e209ccd9..aec91abd390 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -11,11 +11,11 @@ content. Note that file content is Base64 encoded. This endpoint can be accessed
without authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/files
+GET /projects/:id/repository/files/:file_path
```
```bash
-curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master'
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
```
Example response:
@@ -36,17 +36,32 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `ref` (required) - The name of branch, tag or commit
+
+## Get raw file from repository
+
+```
+GET /projects/:id/repository/files/:file_path/raw
+```
+
+```bash
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb/raw?ref=master'
+```
+
+Parameters:
+
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
## Create new file in repository
```
-POST /projects/:id/repository/files
+POST /projects/:id/repository/files/:file_path
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
@@ -60,7 +75,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
@@ -71,11 +86,11 @@ Parameters:
## Update existing file in repository
```
-PUT /projects/:id/repository/files
+PUT /projects/:id/repository/files/:file_path
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
@@ -89,7 +104,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
@@ -109,11 +124,11 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
## Delete existing file in repository
```
-DELETE /projects/:id/repository/files
+DELETE /projects/:id/repository/files/:file_path
```
```bash
-curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
@@ -127,7 +142,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 28610762dca..46f882ce937 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -18,7 +18,7 @@ GET /runners?scope=active
| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners"
```
Example response:
@@ -57,7 +57,7 @@ GET /runners/all?scope=online
| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/all"
```
Example response:
@@ -108,7 +108,7 @@ GET /runners/:id
| `id` | integer | yes | The ID of a runner |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
```
Example response:
@@ -158,7 +158,7 @@ PUT /runners/:id
| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
```
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
Example response:
@@ -207,19 +207,7 @@ DELETE /runners/:id
| `id` | integer | yes | The ID of a runner |
```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
-```
-
-Example response:
-
-```json
-{
- "active": true,
- "description": "test-1-20150125-test",
- "id": 6,
- "is_shared": false,
- "name": null,
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
```
## List project's runners
@@ -237,7 +225,7 @@ GET /projects/:id/runners
| `id` | integer | yes | The ID of a project |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners"
```
Example response:
@@ -275,7 +263,7 @@ POST /projects/:id/runners
| `runner_id` | integer | yes | The ID of a runner |
```
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners" --form "runner_id=9"
```
Example response:
@@ -306,17 +294,5 @@ DELETE /projects/:id/runners/:runner_id
| `runner_id` | integer | yes | The ID of a runner |
```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
-```
-
-Example response:
-
-```json
-{
- "active": true,
- "description": "test-2016-02-01",
- "id": 9,
- "is_shared": false,
- "name": null
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9"
```
diff --git a/doc/api/services.md b/doc/api/services.md
index fba5da6587d..7d4779f1137 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,43 +139,6 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
-## Build-Emails
-
-Get emails for GitLab CI builds.
-
-### Create/Edit Build-Emails service
-
-Set Build-Emails service for a project.
-
-```
-PUT /projects/:id/services/builds-email
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
-
-
-### Delete Build-Emails service
-
-Delete Build-Emails service for a project.
-
-```
-DELETE /projects/:id/services/builds-email
-```
-
-### Get Build-Emails service settings
-
-Get Build-Emails service settings for a project.
-
-```
-GET /projects/:id/services/builds-email
-```
-
## Campfire
Simple web-based real-time group chat
@@ -580,8 +543,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
-
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
@@ -810,3 +772,38 @@ GET /projects/:id/services/teamcity
[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
+
+
+## MockCI
+
+Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
+
+This service is only available when your environment is set to development.
+
+### Create/Edit MockCI service
+
+Set MockCI service for a project.
+
+```
+PUT /projects/:id/services/mock-ci
+```
+
+Parameters:
+
+- `mock_service_url` (**required**) - http://localhost:4004
+
+### Delete MockCI service
+
+Delete MockCI service for a project.
+
+```
+DELETE /projects/:id/services/mock-ci
+```
+
+### Get MockCI service settings
+
+Get MockCI service settings for a project.
+
+```
+GET /projects/:id/services/mock-ci
+```
diff --git a/doc/api/session.md b/doc/api/session.md
index d7809716fbe..056cc32597c 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -21,7 +21,7 @@ POST /session
| `password` | string | yes | The password of the user |
```bash
-curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
+curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
```
Example response:
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ca6b9347877..ad975e2e325 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -13,14 +13,14 @@ GET /application/settings
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings
```
Example response:
```json
{
- "default_projects_limit" : 10,
+ "default_projects_limit" : 100000,
"signup_enabled" : true,
"id" : 1,
"default_branch_protection" : 2,
@@ -32,12 +32,13 @@ Example response:
"updated_at" : "2016-01-04T15:44:55.176Z",
"session_expire_delay" : 10080,
"home_page_url" : null,
- "default_snippet_visibility" : 0,
+ "default_snippet_visibility" : "private",
"domain_whitelist" : [],
"domain_blacklist_enabled" : false,
"domain_blacklist" : [],
"created_at" : "2016-01-04T15:44:55.176Z",
- "default_project_visibility" : 0,
+ "default_project_visibility" : "private",
+ "default_group_visibility" : "private",
"gravatar_enabled" : true,
"sign_in_text" : null,
"container_registry_token_expire_delay": 5,
@@ -59,18 +60,19 @@ PUT /application/settings
| Attribute | Type | Required | Description |
| --------- | ---- | :------: | ----------- |
-| `default_projects_limit` | integer | no | Project limit per user. Default is `10` |
+| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
| `signin_enabled` | boolean | no | Enable login via a GitLab account. Default is `true`. |
| `gravatar_enabled` | boolean | no | Enable Gravatar |
| `sign_in_text` | string | no | Text on login page |
| `home_page_url` | string | no | Redirect to this URL when not logged in |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. |
-| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. |
+| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
-| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
-| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
+| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
+| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
+| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
@@ -88,7 +90,7 @@ PUT /application/settings
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
```
Example response:
@@ -96,7 +98,7 @@ Example response:
```json
{
"id": 1,
- "default_projects_limit": 10,
+ "default_projects_limit": 100000,
"signup_enabled": true,
"signin_enabled": true,
"gravatar_enabled": true,
@@ -108,8 +110,9 @@ Example response:
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
- "default_project_visibility": 1,
- "default_snippet_visibility": 0,
+ "default_project_visibility": "internal",
+ "default_snippet_visibility": "private",
+ "default_group_visibility": "private",
"domain_whitelist": [],
"domain_blacklist_enabled" : false,
"domain_blacklist" : [],
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index 1ae732d40d6..ea10a26bcd0 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/queue_metrics
```
Example response:
@@ -40,7 +40,7 @@ GET /sidekiq/process_metrics
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/process_metrics
```
Example response:
@@ -82,7 +82,7 @@ GET /sidekiq/job_stats
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/job_stats
```
Example response:
@@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/compound_metrics
```
Example response:
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 5a5dc162ffe..e09d930698e 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -5,15 +5,15 @@
### Snippet visibility level
Snippets in GitLab can be either private, internal, or public.
-You can set it with the `visibility_level` field in the snippet.
+You can set it with the `visibility` field in the snippet.
Constants for snippet visibility levels are:
-| Visibility | Visibility level | Description |
-| ---------- | ---------------- | ----------- |
-| Private | `0` | The snippet is visible only to the snippet creator |
-| Internal | `10` | The snippet is visible for any logged in user |
-| Public | `20` | The snippet can be accessed without any authentication |
+| Visibility | Description |
+| ---------- | ----------- |
+| `private` | The snippet is visible only to the snippet creator |
+| `internal` | The snippet is visible for any logged in user |
+| `public` | The snippet can be accessed without any authentication |
## List snippets
@@ -38,7 +38,7 @@ Parameters:
| `id` | Integer | yes | The ID of a snippet |
``` bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1
```
Example response:
@@ -78,11 +78,11 @@ Parameters:
| `title` | String | yes | The title of a snippet |
| `file_name` | String | yes | The name of a snippet file |
| `content` | String | yes | The content of a snippet |
-| `visibility_level` | Integer | yes | The snippet's visibility |
+| `visibility` | String | yes | The snippet's visibility |
``` bash
-curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets
```
Example response:
@@ -123,11 +123,11 @@ Parameters:
| `title` | String | no | The title of a snippet |
| `file_name` | String | no | The name of a snippet file |
| `content` | String | no | The content of a snippet |
-| `visibility_level` | Integer | no | The snippet's visibility |
+| `visibility` | String | no | The snippet's visibility |
``` bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v4/snippets/1
```
Example response:
@@ -154,7 +154,7 @@ Example response:
## Delete snippet
-Deletes an existing snippet.
+Deletes an existing snippet.
```
DELETE /snippets/:id
@@ -168,7 +168,7 @@ Parameters:
```
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/snippets/1"
```
upon successful delete a `204 No content` HTTP code shall be expected, with no data,
@@ -186,7 +186,7 @@ GET /snippets/public
| `page` | Integer | no | the page to retrieve |
``` bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1
```
Example response:
@@ -229,4 +229,3 @@ Example response:
}
]
```
-
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 3fb8b73be6d..bad380794c1 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -20,7 +20,7 @@ GET /hooks
Example request:
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks
```
Example response:
@@ -59,7 +59,7 @@ POST /hooks
Example request:
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/hooks?url=https://gitlab.example.com/hook"
```
Example response:
@@ -90,7 +90,7 @@ GET /hooks/:id
Example request:
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2
```
Example response:
@@ -123,24 +123,5 @@ DELETE /hooks/:id
Example request:
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
-```
-
-Example response:
-
-```json
-{
- "note_events" : false,
- "project_id" : null,
- "enable_ssl_verification" : true,
- "url" : "https://gitlab.example.com/hook",
- "updated_at" : "2015-11-04T20:12:15.931Z",
- "issues_events" : false,
- "merge_requests_events" : false,
- "created_at" : "2015-11-04T20:12:15.931Z",
- "service_id" : null,
- "id" : 2,
- "push_events" : true,
- "tag_push_events" : false
-}
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2
```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 7f78ffc2390..bf350f024f5 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -26,7 +26,7 @@ Parameters:
"committer_email": "jack@example.com",
"id": "2695effb5807a22ff3d138d593fd856244e155e7",
"message": "Initial commit",
- "parents_ids": [
+ "parent_ids": [
"2a4b78934375d7f53875269ffd4f45fd83a84ebe"
]
},
@@ -57,7 +57,7 @@ Parameters:
| `tag_name` | string | yes | The name of the tag |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/tags/v1.0.0
```
Example Response:
@@ -110,7 +110,7 @@ Parameters:
"committer_email": "jack@example.com",
"id": "2695effb5807a22ff3d138d593fd856244e155e7",
"message": "Initial commit",
- "parents_ids": [
+ "parent_ids": [
"2a4b78934375d7f53875269ffd4f45fd83a84ebe"
]
},
@@ -141,11 +141,6 @@ Parameters:
- `id` (required) - The ID of a project
- `tag_name` (required) - The name of a tag
-```json
-{
- "tag_name": "v4.3.0"
-}
-```
## Create a new release
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 8235be92b12..3f2f4ed54e0 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -9,7 +9,7 @@ GET /templates/gitignores
```
```bash
-curl https://gitlab.example.com/api/v3/templates/gitignores
+curl https://gitlab.example.com/api/v4/templates/gitignores
```
Example response:
@@ -566,7 +566,7 @@ GET /templates/gitignores/:key
| `key` | string | yes | The key of the gitignore template |
```bash
-curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby
+curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby
```
Example response:
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
index e120016fbe6..27e8973da58 100644
--- a/doc/api/templates/gitlab_ci_ymls.md
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -9,7 +9,7 @@ GET /templates/gitlab_ci_ymls
```
```bash
-curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls
+curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls
```
Example response:
@@ -107,7 +107,7 @@ GET /templates/gitlab_ci_ymls/:key
| `key` | string | yes | The key of the GitLab CI YML template |
```bash
-curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby
+curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls/Ruby
```
Example response:
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index ae7218cf1bd..33018f0c53f 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -13,7 +13,7 @@ GET /templates/licenses
| `popular` | boolean | no | If passed, returns only popular licenses |
```bash
-curl https://gitlab.example.com/api/v3/templates/licenses?popular=1
+curl https://gitlab.example.com/api/v4/templates/licenses?popular=1
```
Example response:
@@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
the authenticated user will be used to replace the copyright holder placeholder.
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project
```
Example response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index a2fbbc7e1f8..77667a57195 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -22,7 +22,7 @@ Parameters:
| `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos
```
Example Response:
@@ -92,7 +92,7 @@ Example Response:
"updated_at": "2016-06-17T07:47:34.163Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
@@ -165,7 +165,7 @@ Example Response:
"updated_at": "2016-06-17T07:47:34.163Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
@@ -194,7 +194,7 @@ Parameters:
| `id` | integer | yes | The ID of a todo |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/130/mark_as_done
```
Example Response:
@@ -263,7 +263,7 @@ Example Response:
"updated_at": "2016-06-17T07:47:34.163Z",
"due_date": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
@@ -284,7 +284,7 @@ POST /todos/mark_as_done
```
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/donmark_as_donee
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 852c7ac8ec2..14b5c6c713e 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -699,7 +699,7 @@ Parameters:
| `id` | integer | yes | The ID of the user |
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
```
Example response:
@@ -812,8 +812,6 @@ Example response:
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -829,3 +827,99 @@ Example response:
}
]
```
+
+## Retrieve user impersonation tokens
+
+It retrieves every impersonation token of the user. Note that only administrators can do this.
+This function takes pagination parameters `page` and `per_page` to restrict the list of impersonation tokens.
+
+```
+GET /users/:user_id/impersonation_tokens
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `state` | string | no | filter tokens based on state (all, active, inactive) |
+
+Example response:
+```json
+[
+ {
+ "id": 1,
+ "name": "mytoken",
+ "revoked": false,
+ "expires_at": "2017-01-04",
+ "scopes": ['api'],
+ "active": true,
+ "impersonation": true,
+ "token": "9koXpg98eAheJpvBs5tK"
+ }
+]
+```
+
+## Show a user's impersonation token
+
+It shows a user's impersonation token. Note that only administrators can do this.
+
+```
+GET /users/:user_id/impersonation_tokens/:impersonation_token_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
+
+## Create a impersonation token
+
+It creates a new impersonation token. Note that only administrators can do this.
+You are only able to create impersonation tokens to impersonate the user and perform
+both API calls and Git reads and writes. The user will not see these tokens in his profile
+settings page.
+
+```
+POST /users/:user_id/impersonation_tokens
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `name` | string | yes | The name of the impersonation token |
+| `expires_at` | date | no | The expiration date of the impersonation token |
+| `scopes` | array | no | The array of scopes of the impersonation token (api, read_user) |
+
+Example response:
+```json
+{
+ "id": 1,
+ "name": "mytoken",
+ "revoked": false,
+ "expires_at": "2017-01-04",
+ "scopes": ['api'],
+ "active": true,
+ "impersonation": true,
+ "token": "9koXpg98eAheJpvBs5tK"
+}
+```
+
+## Revoke an impersonation token
+
+It revokes an impersonation token. Note that only administrators can revoke impersonation tokens.
+
+```
+DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The ID of the user |
+| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 59d7f0634b2..7f4426ee85d 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -1,20 +1,25 @@
# V3 to V4 version
-Our V4 API version is currently available as *Beta*! It means that V3
-will still be supported and remain unchanged for now, but be aware that the following
-changes are in V4:
+Since GitLab 9.0, API V4 is the preferred version to be used.
-### Changes
+V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
-- Removed `/projects/:search` (use: `/projects?search=x`)
-- `iid` filter has been removed from `projects/:id/issues`
-- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
-- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
-- Project snippets do not return deprecated field `expires_at`
-- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
-- Status 409 returned for POST `project/:id/members` when a member already exists
-- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`
-- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix)
+Below are the changes made between V3 and V4.
+
+### 8.17
+
+- Removed `GET /projects/:search` (use: `GET /projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `GET /projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `GET /projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Endpoints under `GET /projects/merge_request/:id` have been removed (use: `GET /projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723)
+- Endpoints under `GET /projects/:id/keys` have been removed (use `GET /projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+
+### 9.0
+
+- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
+- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
+- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
- `/licences`
- `/licences/:key`
- `/gitignores`
@@ -23,17 +28,55 @@ changes are in V4:
- `/gitignores/:key`
- `/gitlab_ci_ymls/:key`
- `/dockerfiles/:key`
-- Moved `/projects/fork/:id` to `/projects/:id/fork`
-- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done`
-- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters
-- Return pagination headers for all endpoints that return an array
-- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead
-- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)`
-- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource.
-- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR)
-- Renamed param `branch_name` to `branch` on the following endpoints
- - POST `:id/repository/branches`
- - POST `:id/repository/commits`
- - POST/PUT/DELETE `:id/repository/files`
-- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response
-
+- Moved `POST /projects/fork/:id` to `POST /projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
+- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410)
+- Project filters are no longer available as `GET /projects/foo`, but as `GET /projects?foo=true` instead [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962)
+ - `GET /projects/visible` & `GET /projects/all` are consolidated into `GET /projects` and can be used with or without authorization
+ - `GET /projects/owned` moved to `GET /projects?owned=true`
+ - `GET /projects/starred` moved to `GET /projects?starred=true`
+- `GET /projects` returns all projects visible to current user, even if the user is not a member [!9674](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9674)
+ - To get projects the user is a member of, use `GET /projects?membership=true`
+- Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606)
+- Added `POST /environments/:environment_id/stop` to stop an environment [!8808](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8808)
+- Removed `DELETE /projects/:id/deploy_keys/:key_id/disable`. Use `DELETE /projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
+- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371)
+- Make subscription API more RESTful. Use `POST /projects/:id/:subscribable_type/:subscribable_id/subscribe` to subscribe and `POST /projects/:id/:subscribable_type/:subscribable_id/unsubscribe` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
+- Labels filter on `GET /projects/:id/issues` and `GET /issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
+- Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+ - `POST /projects/:id/repository/branches`
+ - `POST /projects/:id/repository/commits`
+ - `POST/PUT/DELETE :id/repository/files`
+- Renamed the `merge_when_build_succeeds` parameter to `merge_when_pipeline_succeeds` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
+ - `PUT /projects/:id/merge_requests/:merge_request_id/merge`
+ - `POST /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
+ - `POST /projects`
+ - `POST /projects/user/:user_id`
+ - `PUT /projects/:id`
+- Renamed `branch_name` to `branch` on `DELETE /projects/:id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
+- Remove `subscribed` field from responses returning list of issues or merge
+ requests. Fetch individual issues or merge requests to obtain the value
+ of `subscribed`
+ [!9661](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9661)
+- Use `visibility` as string parameter everywhere [!9337](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9337)
+- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
+- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
+- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
+- Return 202 with JSON body on async removals on V4 API (`DELETE /projects/:id/repository/merged_branches` and `DELETE /projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
+- `GET /projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
+- Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875)
+- Renamed all `build` references to `job` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Drop `GET /projects/:id/repository/commits/:sha/jobs` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713)
+ - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline`
+ - Require description when creating a new trigger `POST /projects/:id/triggers`
+- Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675)
+- API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
+- API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
+- Change initial page from `0` to `1` on `GET /projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Return correct `Link` header data for `GET /projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637)
+ - Moved `GET /projects/:id/repository/files?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+ - `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
+ - Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
+ - `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
diff --git a/doc/api/version.md b/doc/api/version.md
index 287d17cf97f..8b2a5b51bc5 100644
--- a/doc/api/version.md
+++ b/doc/api/version.md
@@ -10,7 +10,7 @@ GET /version
```
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/version
```
Example response:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index cbab7c9f18d..d8fba5d7a77 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -27,6 +27,8 @@
## Breaking changes
+- [CI variables renaming](variables/README.md#9-0-renaming) Read about the
+ deprecated CI variables and what you should use for GitLab 9.0+.
- [New CI job permissions model](../user/project/new_ci_build_permissions_model.md)
Read about what changed in GitLab 8.12 and how that affects your jobs.
There's a new way to access your Git submodules and LFS objects in jobs.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 6ae6269b28a..f58ab4d87af 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -298,14 +298,14 @@ could look like:
- docker:dind
stage: build
script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- docker build -t registry.example.com/group/project/image:latest .
- docker push registry.example.com/group/project/image:latest
```
You have to use the special `gitlab-ci-token` user created for you in order to
push to the Registry connected to your project. Its password is provided in the
-`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
+`$CI_JOB_TOKEN` variable. This allows you to automate building and deployment
of your Docker images.
You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
@@ -315,10 +315,10 @@ services:
- docker:dind
variables:
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker login -u gitlab-ci-token -p $CI_COMMIT_TOKEN $CI_REGISTRY
build:
stage: build
@@ -328,7 +328,7 @@ build:
```
Here, `$CI_REGISTRY_IMAGE` would be resolved to the address of the registry tied
-to this project, and `$CI_BUILD_REF_NAME` would be resolved to the branch or
+to this project, and `$CI_COMMIT_REF_NAME` would be resolved to the branch or
tag name for this particular job. We also declare our own variable, `$IMAGE_TAG`,
combining the two to save us some typing in the `script` section.
@@ -350,11 +350,11 @@ stages:
- deploy
variables:
- CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_BUILD_REF_NAME
+ CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
before_script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
build:
stage: build
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 00787323b6b..f025a7e3496 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -170,13 +170,17 @@ services:
```
When the job is run, `tutum/wordpress` will be started and you will have
-access to it from your build container under the hostname `tutum__wordpress`.
+access to it from your build container under the hostnames `tutum-wordpress`
+(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`.
-The alias hostname for the service is made from the image name following these
+*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.*
+
+The alias hostnames for the service are made from the image name following these
rules:
1. Everything after `:` is stripped
-2. Slash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`) - primary alias
+3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer
## Configuring services
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3c31ba45d3d..b28f3e13eae 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -263,7 +263,7 @@ This works just like any other terminal - you'll be in the container created
by your deployment, so you can run shell commands and get responses in real
time, check the logs, try out configuration or code tweaks, etc. You can open
multiple terminals to the same environment - they each get their own shell
-session - and even a multiplexer like `screen` or `tmux`!
+session - and even a multiplexer like `screen` or `tmux`!
>**Note:**
Container-based deployments often lack basic tools (like an editor), and may
@@ -295,7 +295,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
@@ -306,22 +306,22 @@ deploy_review:
Let's break it down in pieces. The job's name is `deploy_review` and it runs
on the `deploy` stage. The `script` at this point is fictional, you'd have to
use your own based on your deployment. Then, we set the `environment` with the
-`environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
+`environment:name` being `review/$CI_COMMIT_REF_NAME`. Now that's an interesting
one. Since the [environment name][env-name] can contain slashes (`/`), we can
use this pattern to distinguish between dynamic environments and the regular
ones.
-So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
-which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
+So, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`
+which takes the value of the branch name. Since `$CI_COMMIT_REF_NAME` itself may
also contain `/`, or other characters that would be invalid in a domain name or
URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
environment can get a specific and distinct URL for each branch. In this case,
-given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
-You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
-`https://$CI_BUILD_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+You could also use `$CI_COMMIT_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_COMMIT_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
here because it is guaranteed to be unique, but if you're using a workflow like
[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
environment names to be more closely based on the branch name - the example
@@ -356,7 +356,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
@@ -387,16 +387,16 @@ deploy_prod:
A more realistic example would include copying files to a location where a
webserver (NGINX) could then read and serve. The example below will copy the
-`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`:
+`public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
```yaml
review_app:
stage: deploy
script:
- - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
+ - rsync -av --delete public /srv/nginx/$CI_COMMIT_REF_SLUG
environment:
- name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.example.com
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_COMMIT_REF_SLUG.example.com
```
It is assumed that the user has already setup NGINX and GitLab Runner in the
@@ -526,7 +526,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
on_stop: stop_review
only:
@@ -542,7 +542,7 @@ stop_review:
- echo "Remove review app"
when: manual
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
action: stop
```
@@ -568,13 +568,13 @@ You can read more in the [`.gitlab-ci.yml` reference][onstop].
As we've seen in the [dynamic environments](#dynamic-environments), you can
prepend their name with a word, then followed by a `/` and finally the branch
-name which is automatically defined by the `CI_BUILD_REF_NAME` variable.
+name which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
In short, environments that are named like `type/foo` are presented under a
group named `type`.
-In our minimal example, we name the environments `review/$CI_BUILD_REF_NAME`
-where `$CI_BUILD_REF_NAME` is the branch name:
+In our minimal example, we name the environments `review/$CI_COMMIT_REF_NAME`
+where `$CI_COMMIT_REF_NAME` is the branch name:
```yaml
deploy_review:
@@ -582,7 +582,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
```
In that case, if you visit the Environments page, and provided the branches
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2a5401ac13a..30f209f80eb 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -6,7 +6,7 @@ projects.
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
-and configure your GitLab project to use a [Runner], then each merge request or
+and configure your GitLab project to use a [Runner], then each commit or
push, triggers your CI [pipeline].
The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
@@ -14,8 +14,8 @@ a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
use all three stages; stages with no jobs are simply ignored.
If everything runs OK (no non-zero return values), you'll get a nice green
-checkmark associated with the pushed commit or merge request. This makes it
-easy to see whether a merge request caused any of the tests to fail before
+checkmark associated with the commit. This makes it
+easy to see whether a commit caused any of the tests to fail before
you even look at the code.
Most projects use GitLab's CI service to run the test suite so that
@@ -207,15 +207,6 @@ you expected.
You are also able to view the status of any commit in the various pages in
GitLab, such as **Commits** and **Merge requests**.
-## Enabling build emails
-
-If you want to receive e-mail notifications about the result status of the
-jobs, you should explicitly enable the **Builds Emails** service under your
-project's settings.
-
-For more information read the
-[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
-
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index c679ea4e298..28c484ddbe6 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -80,7 +80,7 @@ The process of adding Review Apps in your workflow would look like:
1. [Install][install-runner] and [configure][conf-runner] a Runner that does
the deployment.
1. Set up a job in `.gitlab-ci.yml` that uses the predefined
- [predefined CI environment variable][variables] `${CI_BUILD_REF_NAME}` to
+ [predefined CI environment variable][variables] `${CI_COMMIT_REF_NAME}` to
create dynamic environments and restrict it to run only on branches.
1. Optionally set a job that [manually stops][manual-env] the Review Apps.
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 49e7ac38b26..befaa06e918 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,14 +30,23 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment
-to the SSH key, or the `before_script` will prompt for a passphrase.
+instructions to [generate an SSH key](../../ssh/README.md). Do not add a
+passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
and in the **Value** field paste the content of your _private_ key that you
created earlier.
+It is also good practice to check the server's own public key to make sure you
+are not being targeted by a man-in-the-middle attack. To do this, add another
+variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run
+the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the
+server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If
+you need to connect to multiple servers, concatenate all the server public keys
+that you collected into the **Value** of the variable. There must be one key per
+line.
+
Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
Add it to the top:
@@ -59,6 +68,11 @@ before_script:
# you will overwrite your user's SSH config.
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+ # In order to properly check the server's host key, assuming you created the
+ # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines
+ # instead.
+ # - mkdir -p ~/.ssh
+ # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
```
As a final step, add the _public_ key from the one you created earlier to the
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 740edba1f59..ccaee33dc92 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -36,7 +36,7 @@ it will not trigger a job.
To trigger a job you need to send a `POST` request to GitLab's API endpoint:
```
-POST /projects/:id/trigger/builds
+POST /projects/:id/trigger/pipeline
```
The required parameters are the trigger's `token` and the Git `ref` on which
@@ -71,7 +71,7 @@ To trigger a job from webhook of another project you need to add the following
webhook url for Push and Tag push events:
```
-https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN
```
> **Note**:
@@ -105,7 +105,7 @@ Using cURL you can trigger a rebuild with minimal effort, for example:
curl --request POST \
--form token=TOKEN \
--form ref=master \
- https://gitlab.example.com/api/v3/projects/9/trigger/builds
+ https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
In this case, the project with ID `9` will get rebuilt on `master` branch.
@@ -114,7 +114,7 @@ Alternatively, you can pass the `token` and `ref` arguments in the query string:
```bash
curl --request POST \
- "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master"
+ "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
```
### Triggering a job within `.gitlab-ci.yml`
@@ -128,7 +128,7 @@ need to add in project's A `.gitlab-ci.yml`:
build_docs:
stage: deploy
script:
- - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
+ - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
only:
- tags
```
@@ -187,7 +187,7 @@ curl --request POST \
--form token=TOKEN \
--form ref=master \
--form "variables[UPLOAD_TO_S3]=true" \
- https://gitlab.example.com/api/v3/projects/9/trigger/builds
+ https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
### Using webhook to trigger job
@@ -195,7 +195,7 @@ curl --request POST \
You can add the following webhook to another project in order to trigger a job:
```
-https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true
```
### Using cron to trigger nightly jobs
@@ -205,7 +205,7 @@ in conjunction with cron. The example below triggers a job on the `master`
branch of project with ID `9` every night at `00:30`:
```bash
-30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 8a638ed3df8..4e9094cb0f1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -27,79 +27,73 @@ Some of the predefined environment variables are available only if a minimum
version of [GitLab Runner][runner] is used. Consult the table below to find the
version of Runner required.
-| Variable | GitLab | Runner | Description |
-|-------------------------|--------|--------|-------------|
-| **CI** | all | 0.4 | Mark that job is executed in CI environment |
-| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
-| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
-| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
-| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
-| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
-| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally |
-| **CI_BUILD_REF** | all | all | The commit revision for which project is built |
-| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. |
-| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built |
-| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository |
-| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered] |
-| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
-| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry |
-| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
-| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built |
-| **CI_PROJECT_NAMESPACE**| 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
-| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
-| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
-| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
-| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
-| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
-| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
-| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
-| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
-| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
-| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
-| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
-| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
-| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
-| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
-
-
-Example values:
-
-```bash
-export CI_BUILD_ID="50"
-export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
-export CI_BUILD_REF_NAME="master"
-export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
-export CI_BUILD_TAG="1.0.0"
-export CI_BUILD_NAME="spec:other"
-export CI_BUILD_STAGE="test"
-export CI_BUILD_MANUAL="true"
-export CI_BUILD_TRIGGERED="true"
-export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
-export CI_PIPELINE_ID="1000"
-export CI_PROJECT_ID="34"
-export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
-export CI_PROJECT_NAME="gitlab-ce"
-export CI_PROJECT_NAMESPACE="gitlab-org"
-export CI_PROJECT_PATH="gitlab-org/gitlab-ce"
-export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce"
-export CI_REGISTRY="registry.example.com"
-export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce"
-export CI_RUNNER_ID="10"
-export CI_RUNNER_DESCRIPTION="my runner"
-export CI_RUNNER_TAGS="docker, linux"
-export CI_SERVER="yes"
-export CI_SERVER_NAME="GitLab"
-export CI_SERVER_REVISION="70606bf"
-export CI_SERVER_VERSION="8.9.0"
-export GITLAB_USER_ID="42"
-export GITLAB_USER_EMAIL="user@example.com"
-```
+>**Note:**
+Starting with GitLab 9.0, we have deprecated some variables. Read the
+[9.0 Renaming](#9-0-renaming) section to find out their replacements. **You are
+strongly advised to use the new variables as we will remove the old ones in
+future GitLab releases.**
+
+| Variable | GitLab | Runner | Description |
+|-------------------------------- |--------|--------|-------------|
+| **CI** | all | 0.4 | Mark that job is executed in CI environment |
+| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
+| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
+| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
+| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
+| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
+| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
+| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry |
+| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
+| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
+| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
+| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
+| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
+| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built |
+| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
+| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
+| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
+| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
+| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
+| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
+| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
+| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
+| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
+| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
+| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
+| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
+| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
+| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
+| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
+
+## 9.0 Renaming
+
+To follow conventions of naming across GitLab, and to futher move away from the
+`build` term and toward `job` CI variables have been renamed for the 9.0
+release.
+
+| 8.x name | 9.0+ name |
+| --------------------- |------------------------ |
+| `CI_BUILD_ID` | `CI_JOB_ID` |
+| `CI_BUILD_REF` | `CI_COMMIT_SHA` |
+| `CI_BUILD_TAG` | `CI_COMMIT_TAG` |
+| `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` |
+| `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` |
+| `CI_BUILD_NAME` | `CI_JOB_NAME` |
+| `CI_BUILD_STAGE` | `CI_JOB_STAGE` |
+| `CI_BUILD_REPO` | `CI_REPOSITORY_URL` |
+| `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` |
+| `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` |
+| `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` |
## `.gitlab-ci.yaml` defined variables
@@ -131,6 +125,16 @@ job_name:
variables: []
```
+You are able to use other variables inside your variable definition (or escape them with `$$`):
+
+```yaml
+variables:
+ LS_CMD: 'ls $FLAGS $$TMP_DIR'
+ FLAGS: '-al'
+script:
+ - 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR'
+```
+
## Secret variables
>**Notes:**
@@ -148,7 +152,8 @@ available in the build environment. It's the recommended method to use for
storing things like passwords, secret keys and credentials.
Secret variables can be added by going to your project's
-**Settings ➔ Variables ➔ Add variable**.
+**Settings ➔ CI/CD Pipelines**, then finding the section called
+**Secret Variables**.
Once you set them, they will be available for all subsequent jobs.
@@ -231,18 +236,18 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI=true
++ export CI_DEBUG_TRACE=false
++ CI_DEBUG_TRACE=false
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
++ export CI_PROJECT_ID=1796893
++ CI_PROJECT_ID=1796893
++ export CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace
@@ -261,20 +266,20 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI=true
++ export GITLAB_CI=true
++ GITLAB_CI=true
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_NAME=debug_trace
-++ CI_BUILD_NAME=debug_trace
-++ export CI_BUILD_STAGE=test
-++ CI_BUILD_STAGE=test
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_COMMIT_NAME=debug_trace
+++ CI_JOB_NAME=debug_trace
+++ export CI_JOB_STAGE=test
+++ CI_JOB_STAGE=test
++ export CI_SERVER_NAME=GitLab
++ CI_SERVER_NAME=GitLab
++ export CI_SERVER_VERSION=8.14.3-ee
@@ -297,8 +302,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_RUNNER_ID=1337
++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
++ CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com
-++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
-++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo, git-annex'
+++ export 'CI_RUNNER_TAGS=shared, docker, linux, ruby, mysql, postgres, mongo'
+++ CI_RUNNER_TAGS='shared, docker, linux, ruby, mysql, postgres, mongo'
++ export CI_REGISTRY=registry.example.com
++ CI_REGISTRY=registry.example.com
++ export CI_DEBUG_TRACE=true
@@ -341,6 +346,41 @@ job_name:
- export
```
+Example values:
+
+```bash
+export CI_JOB_ID="50"
+export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
+export CI_COMMIT_REF_NAME="master"
+export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
+export CI_COMMIT_TAG="1.0.0"
+export CI_JOB_NAME="spec:other"
+export CI_JOB_STAGE="test"
+export CI_JOB_MANUAL="true"
+export CI_JOB_TRIGGERED="true"
+export CI_JOB_TOKEN="abcde-1234ABCD5678ef"
+export CI_PIPELINE_ID="1000"
+export CI_PROJECT_ID="34"
+export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
+export CI_PROJECT_NAME="gitlab-ce"
+export CI_PROJECT_NAMESPACE="gitlab-org"
+export CI_PROJECT_PATH="gitlab-org/gitlab-ce"
+export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-ce"
+export CI_REGISTRY="registry.example.com"
+export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce"
+export CI_RUNNER_ID="10"
+export CI_RUNNER_DESCRIPTION="my runner"
+export CI_RUNNER_TAGS="docker, linux"
+export CI_SERVER="yes"
+export CI_SERVER_NAME="GitLab"
+export CI_SERVER_REVISION="70606bf"
+export CI_SERVER_VERSION="8.9.0"
+export GITLAB_USER_ID="42"
+export GITLAB_USER_EMAIL="user@example.com"
+export CI_REGISTRY_USER="gitlab-ci-token"
+export CI_REGISTRY_PASSWORD="longalfanumstring"
+```
+
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784
[runner]: https://docs.gitlab.com/runner/
[triggered]: ../triggers/README.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a73598df812..ad3ebd144df 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -70,7 +70,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| image | no | Use docker image, covered in [Use Docker](../docker/README.md) |
| services | no | Use docker services, covered in [Use Docker](../docker/README.md) |
| stages | no | Define build stages |
-| types | no | Alias for `stages` |
+| types | no | Alias for `stages` (deprecated) |
| before_script | no | Define commands that run before each job's script |
| after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables |
@@ -130,6 +130,8 @@ There are also two edge cases worth mentioning:
### types
+> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead.
+
Alias for [stages](#stages).
### variables
@@ -151,7 +153,7 @@ thus allowing to fine tune them. Variables can be also defined on a
[job level](#job-variables).
Except for the user defined variables, there are also the ones set up by the
-Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of
+Runner itself. One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called secret variables
which can be set in GitLab's UI.
@@ -166,10 +168,11 @@ which can be set in GitLab's UI.
cached between jobs. You can only use paths that are within the project
workspace.
-**By default the caching is enabled per-job and per-branch.**
+**By default caching is enabled and shared between pipelines and jobs,
+starting from GitLab 9.0**
-If `cache` is defined outside the scope of the jobs, it means it is set
-globally and all jobs will use its definition.
+If `cache` is defined outside the scope of jobs, it means it is set
+globally and all jobs will use that definition.
Cache all files in `binaries` and `.config`:
@@ -202,7 +205,7 @@ rspec:
- binaries/
```
-Locally defined cache overwrites globally defined options. The following `rspec`
+Locally defined cache overrides globally defined options. The following `rspec`
job will cache only `binaries/`:
```yaml
@@ -213,10 +216,15 @@ cache:
rspec:
script: test
cache:
+ key: rspec
paths:
- binaries/
```
+Note that since cache is shared between jobs, if you're using different
+paths for different jobs, you should also set a different **cache:key**
+otherwise cache content can be overwritten.
+
The cache is provided on a best-effort basis, so don't expect that the cache
will be always present. For implementation details, please check GitLab Runner.
@@ -233,6 +241,9 @@ different jobs or even different branches.
The `cache:key` variable can use any of the [predefined variables](../variables/README.md).
+The default key is **default** across the project, therefore everything is
+shared between each pipelines and jobs by default, starting from GitLab 9.0.
+
---
**Example configurations**
@@ -241,7 +252,7 @@ To enable per-job caching:
```yaml
cache:
- key: "$CI_BUILD_NAME"
+ key: "$CI_JOB_NAME"
untracked: true
```
@@ -249,7 +260,7 @@ To enable per-branch caching:
```yaml
cache:
- key: "$CI_BUILD_REF_NAME"
+ key: "$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -257,7 +268,7 @@ To enable per-job and per-branch caching:
```yaml
cache:
- key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME"
+ key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -265,7 +276,7 @@ To enable per-branch and per-stage caching:
```yaml
cache:
- key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME"
+ key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -274,7 +285,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%"
+ key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%"
untracked: true
```
@@ -545,13 +556,30 @@ The above script will:
Manual actions are a special type of job that are not executed automatically;
they need to be explicitly started by a user. Manual actions can be started
-from pipeline, build, environment, and deployment views. You can execute the
-same manual action multiple times.
+from pipeline, build, environment, and deployment views.
An example usage of manual actions is deployment to production.
Read more at the [environments documentation][env-manual].
+Manual actions can be either optional or blocking. Blocking manual action will
+block execution of the pipeline at stage this action is defined in. It is
+possible to resume execution of the pipeline when someone executes a blocking
+manual actions by clicking a _play_ button.
+
+When pipeline is blocked it will not be merged if Merge When Pipeline Succeeds
+is set. Blocked pipelines also do have a special status, called _manual_.
+
+Manual actions are non-blocking by default. If you want to make manual action
+blocking, it is necessary to add `allow_failure: false` to the job's definition
+in `.gitlab-ci.yml`.
+
+Optional manual actions have `allow_failure: true` set by default.
+
+**Statuses of optional actions do not contribute to overall pipeline status.**
+
+> Blocking manual actions were introduced in GitLab 9.0
+
### environment
>
@@ -711,12 +739,12 @@ deploy as review app:
stage: deploy
script: make deploy
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+create the `review/$CI_COMMIT_REF_NAME` environment, where `$CI_COMMIT_REF_NAME`
is an [environment variable][variables] set by the Runner. The
`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
for inclusion in URLs. In this case, if the `deploy as review app` job was run
@@ -822,7 +850,7 @@ To create an archive with a name of the current job:
```yaml
job:
artifacts:
- name: "$CI_BUILD_NAME"
+ name: "$CI_JOB_NAME"
```
To create an archive with a name of the current branch or tag including only
@@ -831,7 +859,7 @@ the files that are untracked by Git:
```yaml
job:
artifacts:
- name: "$CI_BUILD_REF_NAME"
+ name: "$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -841,7 +869,7 @@ tag including only the files that are untracked by Git:
```yaml
job:
artifacts:
- name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
+ name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
untracked: true
```
@@ -850,7 +878,7 @@ To create an archive with a name of the current [stage](#stages) and branch name
```yaml
job:
artifacts:
- name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
+ name: "${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}"
untracked: true
```
@@ -862,7 +890,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
job:
artifacts:
- name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+ name: "%CI_JOB_STAGE%_%CI_COMMIT_REF_NAME%"
untracked: true
```
@@ -1003,6 +1031,9 @@ job:
### coverage
+**Notes:**
+- [Introduced][ce-7447] in GitLab 8.17.
+
`coverage` allows you to configure how code coverage will be extracted from the
job output.
@@ -1015,7 +1046,7 @@ A simple example:
```yaml
job1:
- coverage: /Code coverage: \d+\.\d+/
+ coverage: '/Code coverage: \d+\.\d+/'
```
## Git Strategy
@@ -1361,3 +1392,4 @@ CI with various languages.
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
[variables]: ../variables/README.md
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
+[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
diff --git a/doc/customization/branded_page_and_email_header.md b/doc/customization/branded_page_and_email_header.md
new file mode 100644
index 00000000000..9a0f0b382fa
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header.md
@@ -0,0 +1,15 @@
+# Changing the logo on the overall page and email header
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Upload the custom logo (**Header logo**) in the section **Navigation bar**.
+
+![appearance](branded_page_and_email_header/appearance.png)
+
+After saving the page, your GitLab navigation bar will contain the custom logo:
+
+![custom_brand_header](branded_page_and_email_header/custom_brand_header.png)
+
+The GitLab pipeline emails will also have the custom logo:
+
+![custom_email_header](branded_page_and_email_header/custom_email_header.png)
diff --git a/doc/customization/branded_page_and_email_header/appearance.png b/doc/customization/branded_page_and_email_header/appearance.png
new file mode 100644
index 00000000000..abbba6f9ac9
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/appearance.png
Binary files differ
diff --git a/doc/customization/branded_page_and_email_header/custom_brand_header.png b/doc/customization/branded_page_and_email_header/custom_brand_header.png
new file mode 100644
index 00000000000..7390f8a5e4e
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/custom_brand_header.png
Binary files differ
diff --git a/doc/customization/branded_page_and_email_header/custom_email_header.png b/doc/customization/branded_page_and_email_header/custom_email_header.png
new file mode 100644
index 00000000000..705698ef4a8
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/custom_email_header.png
Binary files differ
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index c71858c6a24..ce39a379a0e 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -1,7 +1,7 @@
-# Generate a changelog entry
+# Changelog entries
-This guide contains instructions for generating a changelog entry data file, as
-well as information and history about our changelog process.
+This guide contains instructions for when and how to generate a changelog entry
+file, as well as information and history about our changelog process.
## Overview
@@ -19,19 +19,71 @@ author: Ozzy Osbourne
The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
-contributors. Both are optional.
+contributors. **Both are optional**.
Community contributors and core team members are encouraged to add their name to
-the `author` field. GitLab team members should not.
-
-If you're working on the GitLab EE repository, the entry will be added to
-`changelogs/unreleased-ee/` instead.
+the `author` field. GitLab team members **should not**.
[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md
[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/
[YAML]: https://en.wikipedia.org/wiki/YAML
-## Instructions
+## What warrants a changelog entry?
+
+- Any user-facing change **should** have a changelog entry. Example: "GitLab now
+ uses system fonts for all text."
+- A fix for a regression introduced and then fixed in the same release (i.e.,
+ fixing a bug introduced during a monthly release candidate) **should not**
+ have a changelog entry.
+- Any developer-facing change (e.g., refactoring, technical debt remediation,
+ test suite changes) **should not** have a changelog entry. Example: "Reduce
+ database records created during Cycle Analytics model spec."
+- _Any_ contribution from a community member, no matter how small, **may** have
+ a changelog entry regardless of these guidelines if the contributor wants one.
+ Example: "Fixed a typo on the search results page. (Jane Smith)"
+
+## Writing good changelog entries
+
+A good changelog entry should be descriptive and concise. It should explain the
+change to a reader who has _zero context_ about the change. If you have trouble
+making it both concise and descriptive, err on the side of descriptive.
+
+- **Bad:** Go to a project order.
+- **Good:** Show a user's starred projects at the top of the "Go to project"
+ dropdown.
+
+The first example provides no context of where the change was made, or why, or
+how it benefits the user.
+
+- **Bad:** Copy [some text] to clipboard.
+- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being
+ copied.
+
+Again, the first example is too vague and provides no context.
+
+- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and
+ builds dropdown.
+- **Good:** Fix tooltips and hover states in mini pipeline graph and builds
+ dropdown.
+
+The first example is too focused on implementation details. The user doesn't
+care that we changed CSS and HTML, they care about the _end result_ of those
+changes.
+
+- **Bad:** Strip out `nil`s in the Array of Commit objects returned from
+ `find_commits_by_message_with_elastic`
+- **Good:** Fix 500 errors caused by elasticsearch results referencing
+ garbage-collected commits
+
+The first example focuses on _how_ we fixed something, not on _what_ it fixes.
+The rewritten version clearly describes the _end benefit_ to the user (fewer 500
+errors), and _when_ (searching commits with ElasticSearch).
+
+Use your best judgement and try to put yourself in the mindset of someone
+reading the compiled changelog. Does this entry add value? Does it offer context
+about _where_ and _why_ the change was made?
+
+## How to generate a changelog entry
A `bin/changelog` script is available to generate the changelog entry file
automatically.
@@ -55,19 +107,28 @@ title: Hey DZ, I added a feature to GitLab!
merge_request:
author:
```
+If you're working on the GitLab EE repository, the entry will be added to
+`changelogs/unreleased-ee/` instead.
+
+#### Arguments
-### Arguments
+| Argument | Shorthand | Purpose |
+| ----------------- | --------- | --------------------------------------------- |
+| [`--amend`] | | Amend the previous commit |
+| [`--force`] | `-f` | Overwrite an existing entry |
+| [`--merge-request`] | `-m` | Set merge request ID |
+| [`--dry-run`] | `-n` | Don't actually write anything, just print |
+| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
+| [`--help`] | `-h` | Print help message |
-| Argument | Shorthand | Purpose |
-| ----------------- | --------- | --------------------------------------------- |
-| `--amend` | | Amend the previous commit |
-| `--force` | `-f` | Overwrite an existing entry |
-| `--merge-request` | `-m` | Merge Request ID |
-| `--dry-run` | `-n` | Don't actually write anything, just print |
-| `--git-username` | `-u` | Use Git user.name configuration as the author |
-| `--help` | `-h` | Print help message |
+[`--amend`]: #-amend
+[`--force`]: #-force-or-f
+[`--merge-request`]: #-merge-request-or-m
+[`--dry-run`]: #-dry-run-or-n
+[`--git-username`]: #-git-username-or-u
+[`--help`]: #-help
-#### `--amend`
+##### `--amend`
You can pass the **`--amend`** argument to automatically stage the generated
file and amend it to the previous commit.
@@ -88,7 +149,7 @@ merge_request:
author:
```
-#### `--force` or `-f`
+##### `--force` or `-f`
Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
already exists.
@@ -105,7 +166,7 @@ merge_request: 1983
author:
```
-#### `--merge-request` or `-m`
+##### `--merge-request` or `-m`
Use the **`--merge-request`** or **`-m`** argument to provide the
`merge_request` value:
@@ -119,7 +180,7 @@ merge_request: 1983
author:
```
-#### `--dry-run` or `-n`
+##### `--dry-run` or `-n`
Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
committing anything:
@@ -135,7 +196,7 @@ author:
$ ls changelogs/unreleased/
```
-#### `--git-username` or `-u`
+##### `--git-username` or `-u`
Use the **`--git-username`** or **`-u`** argument to automatically fill in the
`author` value with your configured Git `user.name` value:
@@ -152,7 +213,7 @@ merge_request:
author: Jane Doe
```
-## History and Reasoning
+### History and Reasoning
Our `CHANGELOG` file was previously updated manually by each contributor that
felt their change warranted an entry. When two merge requests added their own
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index 2f49b3564ab..b03216fec95 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -2,11 +2,12 @@
This document describes what services we use for testing GitLab and GitLab CI.
-We currently use three CI services to test GitLab:
+We currently use four CI services to test GitLab:
1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
+4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development
| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index fc948a7a116..9bed441c131 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -444,7 +444,7 @@ Rendered example:
### cURL commands
-- Use `https://gitlab.example.com/api/v3/` as an endpoint.
+- Use `https://gitlab.example.com/api/v4/` as an endpoint.
- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
- Always put the request first. `GET` is the default so you don't have to
include it.
@@ -468,7 +468,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation.
Get the details of a group:
```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org
```
#### cURL example with parameters passed in the URL
@@ -476,7 +476,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
Create a new project under the authenticated user's namespace:
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo"
```
#### Post data using cURL's --data
@@ -486,7 +486,7 @@ cURL's `--data` option. The example below will create a new project `foo` under
the authenticated user's namespace.
```bash
-curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
```
#### Post data using JSON content
@@ -495,7 +495,7 @@ curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://g
and double quotes.
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
```
#### Post data using form-data
@@ -504,7 +504,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which
properly handles data encoding:
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys
```
The above example is run by and administrator and will add an SSH public key
@@ -518,7 +518,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
ASCII code.
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude"
```
Use `%2F` for slashes (`/`).
@@ -530,7 +530,7 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
`example.net`, you would do something like this:
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings
```
[cURL]: http://curl.haxx.se/ "cURL website"
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index ba47998de49..50105a486d0 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -16,6 +16,44 @@ minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
+### Architecture
+
+The Frontend Architect is an expert who makes high-level frontend design choices
+and decides on technical standards, including coding standards, and frameworks.
+
+When you are assigned a new feature that requires architectural design,
+make sure it is discussed with one of the Frontend Architecture Experts.
+
+This rule also applies if you plan to change the architecture of an existing feature.
+
+These decisions should be accessible to everyone, so please document it on the Merge Request.
+
+You can find the Frontend Architecture experts on the [team page][team-page].
+
+You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
+
+### Realtime
+
+When writing code for realtime features we have to keep a couple of things in mind:
+1. Do not overload the server with requests.
+1. It should feel realtime.
+
+Thus, we must strike a balance between sending requests and the feeling of realtime.
+Use the following rules when creating realtime solutions.
+
+1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
+Use that as your polling interval. This way it is easy for system administrators to change the
+polling rate.
+A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
+1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
+1. Use a common library for polling.
+1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
+Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
+1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
+controlled by the server.
+1. The backend code will most likely be using etags. You do not and should not check for status
+`304 Not Modified`. The browser will transform it for you.
+
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
@@ -50,6 +88,8 @@ Let's look into each of them:
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
+The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
+
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
@@ -70,7 +110,7 @@ You can read more about components in Vue.js site, [Component System][component-
**A folder for the Store**
-The Store is a simple object that allows us to manage the state in a single
+The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
@@ -238,6 +278,9 @@ readability.
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
+- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
+conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
+for specific rules and patterns.
## Testing
@@ -270,7 +313,7 @@ When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
-```
+```javascript
// bad
gl.MyThing = {
@@ -313,6 +356,33 @@ gl.MyThing = MyThing;
```
+### Manipulating the DOM in a JS Class
+
+When writing a class that needs to manipulate the DOM guarantee a container option is provided.
+This is useful when we need that class to be instantiated more than once in the same page.
+
+Bad:
+```javascript
+class Foo {
+ constructor() {
+ document.querySelector('.bar');
+ }
+}
+new Foo();
+```
+
+Good:
+```javascript
+class Foo {
+ constructor(opts) {
+ document.querySelector(`${opts.container} .bar`);
+ }
+}
+
+new Foo({ container: '.my-element' });
+```
+You can find an example of the above in this [class][container-class-example];
+
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
@@ -434,3 +504,8 @@ Scenario: Developer can approve merge request
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
+[airbnb-js-style-guide]: https://github.com/airbnb/javascript
+[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
+[team-page]: https://about.gitlab.com/team
+[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
+[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index b8669964c84..a14c0752366 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -35,7 +35,7 @@ Using this method is in general preferred over directly calling the various
instrumentation methods.
Method instrumentation should be added in the initializer
-`config/initializers/metrics.rb`.
+`config/initializers/8_metrics.rb`.
### Examples
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 2d82b09f301..51b4b398f2c 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -50,6 +50,15 @@ Notes:
asking a GitLab developer to do it once the merge request is merged.
- If you branch is more than 500 commits behind `master`, the job will fail and
you should rebase your branch upon latest `master`.
+- Code reviews for merge requests often consist of multiple iterations of
+ feedback and fixes. There is no need to update your EE MR after each
+ iteration. Instead, create an EE MR as soon as you see the
+ `rake ee_compat_check` job failing. After you receive the final acceptance
+ from a Maintainer (but before the CE MR is merged) update the EE MR.
+ This helps to identify significant conflicts sooner, but also reduces the
+ number of times you have to resolve conflicts.
+- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
+ to avoid resolving the same conflicts multiple times.
## Possible type of conflicts
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 8232a0a113c..2b4126b43ef 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -68,7 +68,7 @@ end
This will end up running one query for every object to update. This code can
easily overload a database given enough rows to update or many instances of this
code running in parallel. This particular problem is known as the
-["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations).
+["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations). You can write a test with [QueryRecoder](query_recorder.md) to detect this and prevent regressions.
In this particular case the workaround is fairly easy:
@@ -117,6 +117,8 @@ Post.all.includes(:author).each do |post|
end
```
+Also consider using [QueryRecoder tests](query_recorder.md) to prevent a regression when eager loading.
+
## Memory Usage
**Summary:** merge requests **must not** increase memory usage unless absolutely
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c1f129e576c..04419650b12 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -39,6 +39,7 @@ GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
+* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
GitLab employees can use GitLab.com's performance monitoring systems located at
<http://performance.gitlab.net>, this requires you to log in using your
diff --git a/doc/development/polling.md b/doc/development/polling.md
new file mode 100644
index 00000000000..a7f2962acf0
--- /dev/null
+++ b/doc/development/polling.md
@@ -0,0 +1,41 @@
+# Polling with ETag caching
+
+Polling for changes (repeatedly asking server if there are any new changes)
+introduces high load on a GitLab instance, because it usually requires
+executing at least a few SQL queries. This makes scaling large GitLab
+instances (like GitLab.com) very difficult so we do not allow adding new
+features that require polling and hit the database.
+
+Instead you should use polling mechanism with ETag caching in Redis.
+
+## How to use it
+
+1. Add the path of the endpoint which you want to poll to
+ `Gitlab::EtagCaching::Middleware`.
+1. Implement cache invalidation for the path of your endpoint using
+ `Gitlab::EtagCaching::Store`. Whenever a resource changes you
+ have to invalidate the ETag for the path that depends on this
+ resource.
+1. Check that the mechanism works:
+ - requests should return status code 304
+ - there should be no SQL queries logged in `log/development.log`
+
+## How it works
+
+1. Whenever a resource changes we generate a random value and store it in
+ Redis.
+1. When a client makes a request we set the `ETag` response header to the value
+ from Redis.
+1. The client caches the response (client-side caching) and sends the ETag as
+ the `If-None-Match` header with every subsequent request for the same
+ resource.
+1. If the `If-None-Match` header matches the current value in Redis we know
+ that the resource did not change so we can send 304 response immediately,
+ without querying the database at all. The client's browser will use the
+ cached response.
+1. If the `If-None-Match` header does not match the current value in Redis
+ we have to generate a new response, because the resource changed.
+
+For more information see:
+- [RFC 7232](https://tools.ietf.org/html/rfc7232)
+- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index e244ad4e881..933033a09e0 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -25,3 +25,5 @@ starting GitLab. For example:
Bullet will log query problems to both the Rails log as well as the Chrome
console.
+
+As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
new file mode 100644
index 00000000000..e0127aaed4c
--- /dev/null
+++ b/doc/development/query_recorder.md
@@ -0,0 +1,29 @@
+# QueryRecorder
+
+QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests.
+
+> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
+
+As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
+
+## How it works
+
+This style of test works by counting the number of SQL queries executed by ActiveRecord. First a control count is taken, then you add new records to the database and rerun the count. If the number of queries has significantly increased then an `N+1` queries problem exists.
+
+```ruby
+it "avoids N+1 database queries" do
+ control_count = ActiveRecord::QueryRecorder.new { visit_some_page }.count
+ create_list(:issue, 5)
+ expect { visit_some_page }.not_to exceed_query_limit(control_count)
+end
+```
+
+As an example you might create 5 issues in between counts, which would cause the query count to increase by 5 if an N+1 problem exists.
+
+> **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible.
+
+## See also
+
+- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
+- [Performance guidelines](performance.md)
+- [Merge request performance guidelines](merge_request_performance_guidelines.md#query-counts) \ No newline at end of file
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 9b545d7f0f1..5ac7b8dadeb 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -35,8 +35,8 @@ GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec karma`.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
- `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
- `spec/javascripts/behaviors/autosize_spec.js.es6` file.
+ `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` has a corresponding
+ `spec/javascripts/behaviors/autosize_spec.js` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index ead79ba6a10..794c8eb6bfe 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen
#### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
+## Confirmation dialogs
+
+- Destruction buttons should be clear and always say what they are destroying.
+ E.g., `Delete page` instead of just `Delete`.
+- If the copy describes another action the user can take instead of the
+ destructive one, provide a way for them to do that as a secondary button.
+- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
+ confusing when you then see the `Cancel` button.
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png
new file mode 100644
index 00000000000..2e356c99762
--- /dev/null
+++ b/doc/development/ux_guide/img/karolina-plaskaty.png
Binary files differ
diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png
new file mode 100644
index 00000000000..01ba0391630
--- /dev/null
+++ b/doc/development/ux_guide/img/nazim-ramesh.png
Binary files differ
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index da410a8de7a..cbd7c17de41 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -14,7 +14,7 @@
### Nazim Ramesh
- Small to medium size organisations using GitLab CE
-<img src="img/steven-lyons.png" width="300px">
+<img src="img/nazim-ramesh.png" width="300px">
#### Demographics
@@ -27,19 +27,19 @@
- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
#### Motivations
-Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab:
+Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
>
“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.”
>
-In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
+In his role as a full-stack web developer, Nazim could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Nazim recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
>
“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.”
>
-Undeterred, Steven decided to migrate a couple of projects across to GitLab.
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
>
“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.”
@@ -47,17 +47,17 @@ Undeterred, Steven decided to migrate a couple of projects across to GitLab.
Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
-The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab.
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
#### Frustrations
##### Adoption to GitLab has been slow
-Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
+Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
##### Missing Features
-Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
+Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
##### Regressions and bugs
-Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
+Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
##### Uses too much RAM and CPU
>
@@ -65,7 +65,7 @@ Steven often has to calm down his colleagues, when a release contains regression
>
##### UI/UX
-GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
+GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
#### Goals
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
@@ -121,8 +121,8 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
#### Goals
* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
-* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily.
-* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven.
+* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
+* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
<hr>
@@ -131,7 +131,7 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
- Would like to use GitLab at work
- Working for a medium to large size organisation
-<img src="img/harry-robison.png" width="300px">
+<img src="img/karolina-plaskaty.png" width="300px">
#### Demographics
@@ -144,21 +144,21 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
#### Motivations
-Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” He’s also an avid reader of GitLab’s blog.
+Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” She’s also an avid reader of GitLab’s blog.
-Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms.
+Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
#### Frustrations
##### Unable to use GitLab at work
-Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab.
+Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
##### Performance
-GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog” which has deterred him from running GitLab on his own machine for just hobby / personal projects.
+GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog” which has deterred her from running GitLab on her own machine for just hobby / personal projects.
##### UX/UI
-Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this.
+Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
#### Goals
-* To develop his programming experience and to learn from other developers.
-* To contribute to both his own and other open source projects.
+* To develop her programming experience and to learn from other developers.
+* To contribute to both her own and other open source projects.
* To use a fast and intuitive version control platform. \ No newline at end of file
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index a6d22e5a04a..fe4b6d73771 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,13 +15,6 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users.
-### Git Annex
-
-Git Annex is also only available on the Enterprise Edition. This means that if
-you have repositories that use Git Annex to store large files, these files will
-no longer be easily available via Git. You should consider migrating these
-repositories to use Git LFS before downgrading to the Community Edition.
-
### Remove Jenkins CI Service entries from the database
The `JenkinsService` class is only available on the Enterprise Edition codebase,
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
index a19f0e57b56..8d7a69e55ed 100644
--- a/doc/gitlab-basics/img/create_new_project_button.png
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 2d2fd8cb380..d35709266e4 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,9 +1,32 @@
# Installation
-- [Installation](installation.md)
-- [Requirements](requirements.md)
-- [Structure](structure.md)
-- [Database MySQL](database_mysql.md)
-- [Digital Ocean and Docker](digitaloceandocker.md)
-- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
-- [All installation methods](https://about.gitlab.com/installation/)
+GitLab can be installed via various ways. Check the [installation methods][methods]
+for an overview.
+
+## Requirements
+
+Before installing GitLab, make sure to check the [requirements documentation](requirements.md)
+which includes useful information on the supported Operating Systems as well as
+the hardware requirements.
+
+## Installation methods
+
+- [Installation using the Omnibus packages](https://about.gitlab.com/downloads/) -
+ Install GitLab using our official deb/rpm repositories. This is the
+ recommended way.
+- [Installation from source](installation.md) - Install GitLab from source.
+ Useful for unsupported systems like *BSD. For an overview of the directory
+ structure, read the [structure documentation](structure.md).
+- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
+- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
+ GitLab on Google Cloud Platform using our official image.
+- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly
+ on DigitalOcean using Docker.
+
+## Database
+
+While the recommended database is PostgreSQL, we provide information to install
+GitLab using MySQL. Check the [MySQL documentation](database_mysql.md) for more
+information.
+
+[methods]: https://about.gitlab.com/installation/
diff --git a/doc/install/google-protobuf.md b/doc/install/google-protobuf.md
new file mode 100644
index 00000000000..a531b4519b3
--- /dev/null
+++ b/doc/install/google-protobuf.md
@@ -0,0 +1,26 @@
+# Installing a locally compiled google-protobuf gem
+
+First we must find the exact version of google-protobuf that your
+GitLab installation requires.
+
+ cd /home/git/gitlab
+
+ # Only one of the following two commands will print something. It
+ # will look like: * google-protobuf (3.2.0)
+ bundle list | grep google-protobuf
+ bundle check | grep google-protobuf
+
+Below we use `3.2.0` as an example. Replace it with the version number
+you found above.
+
+ cd /home/git/gitlab
+ sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby
+
+Finally, you can test whether google-protobuf loads correctly. The
+following should print 'OK'.
+
+ sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK'
+
+If the `gem install` command fails you may need to install developer
+tools. On Debian: `apt-get install build-essential libgmp-dev`, on
+Centos/RedHat `yum groupinstall 'Development Tools'`.
diff --git a/doc/install/google_cloud_platform/img/change_admin_passwd_email.png b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png
new file mode 100644
index 00000000000..1ffe14f60ff
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/chrome_not_secure_page.png b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png
new file mode 100644
index 00000000000..e732066908f
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png
new file mode 100644
index 00000000000..2a1859da6e3
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png
new file mode 100644
index 00000000000..1c4c870dbc9
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gcp_landing.png b/doc/install/google_cloud_platform/img/gcp_landing.png
new file mode 100644
index 00000000000..6398d247ba0
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gcp_landing.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png
new file mode 100644
index 00000000000..f492888ea4a
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png
new file mode 100644
index 00000000000..b38af3966e2
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_deployed_page.png b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png
new file mode 100644
index 00000000000..fef9ae45f32
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png
new file mode 100644
index 00000000000..381c0fe48a5
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/gitlab_launch_button.png b/doc/install/google_cloud_platform/img/gitlab_launch_button.png
new file mode 100644
index 00000000000..50f66f66118
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/gitlab_launch_button.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png
new file mode 100644
index 00000000000..00060841619
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/img/ssh_via_button.png b/doc/install/google_cloud_platform/img/ssh_via_button.png
new file mode 100644
index 00000000000..26106f159ad
--- /dev/null
+++ b/doc/install/google_cloud_platform/img/ssh_via_button.png
Binary files differ
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
new file mode 100644
index 00000000000..26506111548
--- /dev/null
+++ b/doc/install/google_cloud_platform/index.md
@@ -0,0 +1,168 @@
+# Installing GitLab on Google Cloud Platform
+
+![GCP landing page](img/gcp_landing.png)
+
+The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through
+the [Google Cloud Launcher][launcher] program.
+
+## Prerequisites
+
+There are only two prerequisites in order to install GitLab on GCP:
+
+1. You need to have a Google account.
+1. You need to sign up for the GCP program. If this is your first time, Google
+ gives you [$300 credit for free][freetrial] to consume over a 60-day period.
+
+Once you have performed those two steps, you can visit the
+[GCP launcher console][console] which has a list of all the things you can
+deploy on GCP.
+
+![GCP launcher console](img/gcp_launcher_console_home_page.png)
+
+The next step is to find and install GitLab.
+
+## Configuring and deploying the VM
+
+To deploy GitLab on GCP you need to follow five simple steps:
+
+1. Go to https://cloud.google.com/launcher and login with your Google credentials
+1. Search for GitLab from GitLab Inc. (not the same as Bitnami) and click on
+ the tile.
+
+ ![Search for GitLab](img/gcp_search_for_gitlab.png)
+
+1. In the next page, you can see an overview of the GitLab VM as well as some
+ estimated costs. Click the **Launch on Compute Engine** button to choose the
+ hardware and network settings.
+
+ ![Launch on Compute Engine](img/gcp_gitlab_overview.png)
+
+1. In the settings page you can choose things like the datacenter where your GitLab
+ server will be hosted, the number of CPUs and amount of RAM, the disk size
+ and type, etc. Read GitLab's [requirements documentation][req] for more
+ details on what to choose depending on your needs.
+
+ ![Deploy settings](img/new_gitlab_deployment_settings.png)
+
+1. As a last step, hit **Deploy** when ready. The process will finish in a few
+ seconds.
+
+ ![Deploy in progress](img/gcp_gitlab_being_deployed.png)
+
+
+## Visiting GitLab for the first time
+
+After a few seconds, GitLab will be successfully deployed and you should be
+able to see the IP address that Google assigned to the VM, as well as the
+credentials to the GitLab admin account.
+
+![Deploy settings](img/gitlab_deployed_page.png)
+
+1. Click on the IP under **Site address** to visit GitLab.
+1. Accept the self-signed certificate that Google automatically deployed in
+ order to securely reach GitLab's login page.
+1. Use the username and password that are present in the Google console page
+ to login into GitLab and click **Sign in**.
+
+ ![GitLab first sign in](img/gitlab_first_sign_in.png)
+
+Congratulations! GitLab is now installed and you can access it via your browser,
+but we're not done yet. There are some steps you need to take in order to have
+a fully functional GitLab installation.
+
+## Next steps
+
+These are the most important next steps to take after you installed GitLab for
+the first time.
+
+### Changing the admin password and email
+
+Google assigned a random password for the GitLab admin account and you should
+change it ASAP:
+
+1. Visit the GitLab admin page through the link in the Google console under
+ **Admin URL**.
+1. Find the Administrator user under the **Users** page and hit **Edit**.
+1. Change the email address to a real one and enter a new password.
+
+ ![Change GitLab admin password](img/change_admin_passwd_email.png)
+
+1. Hit **Save changes** for the changes to take effect.
+1. After changing the password, you will be signed out from GitLab. Use the
+ new credentials to login again.
+
+### Assigning a static IP
+
+By default, Google assigns an ephemeral IP to your instance. It is strongly
+recommended to assign a static IP if you are going to use GitLab in production
+and use a domain name as we'll see below.
+
+Read Google's documentation on how to [promote an ephemeral IP address][ip].
+
+### Using a domain name
+
+Assuming you have a domain name in your possession and you have correctly
+set up DNS to point to the static IP you configured in the previous step,
+here's how you configure GitLab to be aware of the change:
+
+1. SSH into the VM. You can easily use the **SSH** button in the Google console
+ and a new window will pop up.
+
+ ![SSH button](img/ssh_via_button.png)
+
+ In the future you might want to set up [connecting with an SSH key][ssh]
+ instead.
+
+1. Edit the config file of Omnibus GitLab using your favorite text editor:
+
+ ```
+ sudo vim /etc/gitlab/gitlab.rb
+ ```
+
+1. Set the `external_url` value to the domain name you wish GitLab to have
+ **without** `https`:
+
+ ```
+ external_url 'http://gitlab.example.com'
+ ```
+
+ We will set up HTTPS in the next step, no need to do this now.
+
+1. Reconfigure GitLab for the changes to take effect:
+
+ ```
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. You can now visit GitLab using the domain name.
+
+### Configuring HTTPS with the domain name
+
+Although not needed, it's strongly recommended to secure GitLab with a TLS
+certificate. Follow the steps in the [Omnibus documentation][omni-ssl].
+
+### Configuring the email SMTP settings
+
+You need to configure the email SMTP settings correctly otherwise GitLab will
+not be able to send notification emails, like comments, and password changes.
+Check the [Omnibus documentation][omni-smtp] how to do so.
+
+## Further reading
+
+GitLab can be configured to authenticate with other OAuth providers, LDAP, SAML,
+Kerberos, etc. Here are some documents you might be interested in reading:
+
+- [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/)
+- [Integration documentation](https://docs.gitlab.com/ce/integration/)
+- [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html)
+- [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html)
+
+[console]: https://console.cloud.google.com/launcher "GCP launcher console"
+[freetrial]: https://console.cloud.google.com/freetrial "GCP free trial"
+[ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses"
+[gcp]: https://cloud.google.com/ "Google Cloud Platform"
+[launcher]: https://cloud.google.com/launcher/ "Google Cloud Launcher home page"
+[req]: ../requirements.md "GitLab hardware and software requirements"
+[ssh]: https://cloud.google.com/compute/docs/instances/connecting-to-instance "Connecting to Linux Instances"
+[omni-smtp]: https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings "Omnibus GitLab SMTP settings"
+[omni-ssl]: https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https "Omnibus GitLab enable HTTPS"
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 5ba338ba7d1..177e1a9378b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -155,10 +155,9 @@ page](https://golang.org/dl).
## 4. Node
Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile
-javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to
-manage javascript dependencies. In many distros the versions provided by the
-official package repositories are out of date, so we'll need to install through
-the following commands:
+javascript assets, and yarn >= v0.17.0 to manage javascript dependencies.
+In many distros the versions provided by the official package repositories
+are out of date, so we'll need to install through the following commands:
# install node v7.x
curl --location https://deb.nodesource.com/setup_7.x | bash -
@@ -289,9 +288,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab
-**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -659,6 +658,12 @@ misconfigured gitlab-workhorse instance. Double-check that you've
[installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse),
and correctly [configured Nginx](#site-configuration).
+### google-protobuf "LoadError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found"
+
+This can happen on some platforms for some versions of the
+google-protobuf gem. The workaround is to [install a source-only
+version of this gem](google-protobuf.md).
+
[RVM]: https://rvm.io/ "RVM Homepage"
[rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub"
[chruby]: https://github.com/postmodern/chruby "chruby on GitHub"
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3f90597ec80..7b586138f42 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -108,7 +108,7 @@ use the CI features.
## Unicorn Workers
-It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
+It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
@@ -148,8 +148,18 @@ Sidekiq processes the background jobs with a multithreaded process.
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory.
+## Prometheus and its exporters
+
+As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related
+exporters are enabled by default, to enable easy and in depth monitoring of
+GitLab. Approximately 200MB of memory will be consumed by these processes, with
+default settings.
+
+If you would like to disable Prometheus and it's exporters or read more information
+about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md).
+
## Supported web browsers
We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
-Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version. \ No newline at end of file
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 22bdf33443d..e56e58498a6 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services.
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
+- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index 212b4854dd7..c39d7ab57c6 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -54,7 +54,7 @@ for initial settings.
gitlab_rails['omniauth_providers'] = [
{
"name" => "auth0",
- "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'',
+ "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
namespace: 'YOUR_AUTH0_DOMAIN'
}
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index f8370cd349e..2bc526dc3db 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -1,63 +1 @@
-# Crowd OmniAuth Provider
-
-To enable the Crowd OmniAuth provider you must register your application with Crowd. To configure Crowd integration you need an application name and password.
-
-1. On your GitLab server, open the configuration file.
-
- For omnibus package:
-
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
- ```
-
- For installations from source:
-
- ```sh
- cd /home/git/gitlab
-
- sudo -u git -H editor config/gitlab.yml
- ```
-
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
-
-1. Add the provider configuration:
-
- For omnibus package:
-
- ```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "crowd",
- "args" => {
- "crowd_server_url" => "CROWD",
- "application_name" => "YOUR_APP_NAME",
- "application_password" => "YOUR_APP_PASSWORD"
- }
- }
- ]
- ```
-
- For installations from source:
-
- ```
- - { name: 'crowd',
- args: {
- crowd_server_url: 'CROWD SERVER URL',
- application_name: 'YOUR_APP_NAME',
- application_password: 'YOUR_APP_PASSWORD' } }
- ```
-
-1. Change 'YOUR_APP_NAME' to the application name from Crowd applications page.
-
-1. Change 'YOUR_APP_PASSWORD' to the application password you've set.
-
-1. Save the configuration file.
-
-1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
- installed GitLab via Omnibus or from source respectively.
-
-On the sign in page there should now be a Crowd tab in the sign in form.
-
-[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
-
+This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md).
diff --git a/doc/integration/github.md b/doc/integration/github.md
index cea85f073cc..4b0d33334bd 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use.
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
- Application description: Fill this in if you wish.
- - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
+ - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port.
1. Select "Register application".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 47e20d7566a..6c11f46a70a 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -27,7 +27,7 @@ contains some settings that are common for all providers.
- [Twitter](twitter.md)
- [Shibboleth](shibboleth.md)
- [SAML](saml.md)
-- [Crowd](crowd.md)
+- [Crowd](../administration/auth/crowd.md)
- [Azure](azure.md)
- [Auth0](auth0.md)
- [Authentiq](../administration/auth/authentiq.md)
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
new file mode 100644
index 00000000000..56f367d841e
--- /dev/null
+++ b/doc/integration/openid_connect_provider.md
@@ -0,0 +1,47 @@
+# GitLab as OpenID Connect identity provider
+
+This document is about using GitLab as an OpenID Connect identity provider
+to sign in to other services.
+
+## Introduction to OpenID Connect
+
+[OpenID Connect] \(OIC) is a simple identity layer on top of the
+OAuth 2.0 protocol. It allows clients to verify the identity of the end-user
+based on the authentication performed by GitLab, as well as to obtain
+basic profile information about the end-user in an interoperable and
+REST-like manner. OIC performs many of the same tasks as OpenID 2.0,
+but does so in a way that is API-friendly, and usable by native and
+mobile applications.
+
+On the client side, you can use [omniauth-openid-connect] for Rails
+applications, or any of the other available [client implementations].
+
+GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer
+to its README for more details about which parts of the specifications
+are supported.
+
+## Enabling OpenID Connect for OAuth applications
+
+Refer to the [OAuth guide] for basic information on how to set up OAuth
+applications in GitLab. To enable OIC for an application, all you have to do
+is select the `openid` scope in the application settings.
+
+Currently the following user information is shared with clients:
+
+| Claim | Type | Description |
+|:-----------------|:----------|:------------|
+| `sub` | `string` | An opaque token that uniquely identifies the user
+| `auth_time` | `integer` | The timestamp for the user's last authentication
+| `name` | `string` | The user's full name
+| `nickname` | `string` | The user's GitLab username
+| `email` | `string` | The user's public email address
+| `email_verified` | `boolean` | Whether the user's public email address was verified
+| `website` | `string` | URL for the user's website
+| `profile` | `string` | URL for the user's GitLab profile
+| `picture` | `string` | URL for the user's GitLab avatar
+
+[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website"
+[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website"
+[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider"
+[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website"
+[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations"
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 7a809eddac0..2277aa827b7 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -74,7 +74,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
@@ -91,7 +91,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
@@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
} }
```
@@ -227,7 +227,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] }
}
```
@@ -245,7 +245,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] },
allowed_clock_drift: 1 # for one second clock drift
}
diff --git a/doc/pages/README.md b/doc/pages/README.md
index c9715eed598..7878bce3f10 100644
--- a/doc/pages/README.md
+++ b/doc/pages/README.md
@@ -1 +1 @@
-This document was moved to [user/project/pages](../user/project/pages/index.md).
+This document was moved to [pages/index.md](../user/project/pages/index.md).
diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md
new file mode 100644
index 00000000000..1d63ccb4d2f
--- /dev/null
+++ b/doc/pages/getting_started_part_one.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_one.md).
diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md
new file mode 100644
index 00000000000..1697b5cd6b4
--- /dev/null
+++ b/doc/pages/getting_started_part_three.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_three.md).
diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md
new file mode 100644
index 00000000000..a58affec73d
--- /dev/null
+++ b/doc/pages/getting_started_part_two.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_two.md).
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
deleted file mode 100644
index ee54d865225..00000000000
--- a/doc/project_services/builds_emails.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index a5b8cd6455c..65fcfc77ab1 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -38,23 +38,6 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
-You can specify that portions of the application data be skipped using the
-environment variable `SKIP`. You can skip:
-
-- `db` (database)
-- `uploads` (attachments)
-- `repositories` (Git repositories data)
-- `builds` (CI job output logs)
-- `artifacts` (CI job artifacts)
-- `lfs` (LFS objects)
-- `registry` (Container Registry images)
-
-Separate multiple data types to skip using a comma. For example:
-
-```
-sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
-```
-
Example output:
```
@@ -111,13 +94,14 @@ To use the `copy` strategy instead of the default streaming strategy, specify
You can choose what should be backed up by adding the environment variable `SKIP`.
The available options are:
-* `db`
-* `uploads` (attachments)
-* `repositories`
-* `builds` (CI build output logs)
-* `artifacts` (CI build artifacts)
-* `lfs` (LFS objects)
-* `pages` (pages content)
+- `db` (database)
+- `uploads` (attachments)
+- `repositories` (Git repositories data)
+- `builds` (CI job output logs)
+- `artifacts` (CI job artifacts)
+- `lfs` (LFS objects)
+- `registry` (Container Registry images)
+- `pages` (Pages content)
Use a comma to specify several options at the same time:
@@ -175,6 +159,8 @@ For installations from source:
remote_directory: 'my.s3.bucket'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# encryption: 'AES256'
+ # Specifies Amazon S3 storage class to use for backups, this is optional
+ # storage_class: 'STANDARD'
```
If you are uploading your backups to S3 you will probably want to create a new
@@ -416,7 +402,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true
If there is a GitLab version mismatch between your backup tar file and the installed
version of GitLab, the restore command will abort with an error. Install the
-[correct GitLab version](https://www.gitlab.com/downloads/archives/) and try again.
+[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again.
## Configure cron to make daily backups
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9e391d647a8..cf28f1a2eca 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -13,7 +13,7 @@ read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community
## Locating an existing SSH key pair
-Before generating a new SSH key check if your system already has one
+Before generating a new SSH key pair check if your system already has one
at the default location by opening a shell, or Command Prompt on Windows,
and running the following command:
@@ -23,43 +23,49 @@ and running the following command:
type %userprofile%\.ssh\id_rsa.pub
```
-**GNU/Linux / macOS / PowerShell:**
+**Git Bash on Windows / GNU/Linux / macOS / PowerShell:**
```bash
cat ~/.ssh/id_rsa.pub
```
If you see a string starting with `ssh-rsa` you already have an SSH key pair
-and you can skip the next step **Generating a new SSH key pair**
-and continue onto **Copying your public SSH key to the clipboard**.
+and you can skip the generate portion of the next section and skip to the copy
+to clipboard step.
If you don't see the string or would like to generate a SSH key pair with a
custom name continue onto the next step.
+>
+**Note:** Public SSH key may also be named as follows:
+- `id_dsa.pub`
+- `id_ecdsa.pub`
+- `id_ed25519.pub`
+
## Generating a new SSH key pair
-1. To generate a new SSH key, use the following command:
+1. To generate a new SSH key pair, use the following command:
- **GNU/Linux / macOS:**
+ **Git Bash on Windows / GNU/Linux / macOS:**
```bash
- ssh-keygen -t rsa -C "GitLab" -b 4096
+ ssh-keygen -t rsa -C "your.email@example.com" -b 4096
```
**Windows:**
- On Windows you will need to download
+ Alternatively on Windows you can download
[PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
- and follow this [documentation article][winputty] to generate a SSH key pair.
+ and follow [this documentation article][winputty] to generate a SSH key pair.
-1. Next, you will be prompted to input a file path to save your key pair to.
+1. Next, you will be prompted to input a file path to save your SSH key pair to.
If you don't already have an SSH key pair use the suggested path by pressing
- enter. Using the suggested path will allow your SSH client
- to automatically use the key pair with no additional configuration.
+ enter. Using the suggested path will normally allow your SSH client
+ to automatically use the SSH key pair with no additional configuration.
- If you already have a key pair with the suggested file path, you will need
- to input a new file path and declare what host this key pair will be used
- for in your `.ssh/config` file, see **Working with non-default SSH key pair paths**
+ If you already have a SSH key pair with the suggested file path, you will need
+ to input a new file path and declare what host this SSH key pair will be used
+ for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths)
for more information.
1. Once you have input a file path you will be prompted to input a password to
@@ -68,12 +74,12 @@ custom name continue onto the next step.
pressing enter.
>**Note:**
- If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`.
+ If you want to change the password of your SSH key pair, you can use
+ `ssh-keygen -p <keyname>`.
-1. The next step is to copy the public key as we will need it afterwards.
+1. The next step is to copy the public SSH key as we will need it afterwards.
- To copy your public key to the clipboard, use the appropriate code for your
- operating system below:
+ To copy your public SSH key to the clipboard, use the appropriate code below:
**macOS:**
@@ -93,7 +99,7 @@ custom name continue onto the next step.
type %userprofile%\.ssh\id_rsa.pub | clip
```
- **Windows PowerShell:**
+ **Git Bash on Windows / Windows PowerShell:**
```bash
cat ~/.ssh/id_rsa.pub | clip
@@ -101,22 +107,38 @@ custom name continue onto the next step.
1. The final step is to add your public SSH key to GitLab.
- Navigate to the 'SSH Keys' tab in you 'Profile Settings'.
+ Navigate to the 'SSH Keys' tab in your 'Profile Settings'.
Paste your key in the 'Key' section and give it a relevant 'Title'.
Use an identifiable title like 'Work Laptop - Windows 7' or
'Home MacBook Pro 15'.
If you manually copied your public SSH key make sure you copied the entire
key starting with `ssh-rsa` and ending with your email.
+
+1. Optionally you can test your setup by running `ssh -T git@example.com`
+ (replacing `example.com` with your GitLab domain) and verifying that you
+ receive a `Welcome to GitLab` message.
## Working with non-default SSH key pair paths
If you used a non-default file path for your GitLab SSH key pair,
-you must configure your SSH client to find your GitLab SSH private key
-for connections to your GitLab server (perhaps gitlab.com).
+you must configure your SSH client to find your GitLab private SSH key
+for connections to your GitLab server (perhaps `gitlab.com`).
+
+For your current terminal session you can do so using the following commands
+(replacing `other_id_rsa` with your private SSH key):
-For OpenSSH clients this is configured in the `~/.ssh/config` file.
-Below are two example host configurations using their own key:
+**Git Bash on Windows / GNU/Linux / macOS:**
+
+```bash
+eval $(ssh-agent -s)
+ssh-add ~/.ssh/other_id_rsa
+```
+
+To retain these settings you'll need to save them to a configuration file.
+For OpenSSH clients this is configured in the `~/.ssh/config` file for some
+operating systems.
+Below are two example host configurations using their own SSH key:
```
# GitLab.com server
@@ -140,19 +162,20 @@ That's why it needs to uniquely map to a single user.
## Deploy keys
-Deploy keys allow read-only access to multiple projects with a single SSH
-key.
+Deploy keys allow read-only or read-write (if enabled) access to one or
+multiple projects with a single SSH key pair.
This is really useful for cloning repositories to your Continuous
Integration (CI) server. By using deploy keys, you don't have to setup a
dummy user account.
If you are a project master or owner, you can add a deploy key in the
-project settings under the section 'Deploy Keys'. Press the 'New Deploy
-Key' button and upload a public SSH key. After this, the machine that uses
-the corresponding private key has read-only access to the project.
+project settings under the section 'Repository'. Specify a title for the new
+deploy key and paste a public SSH key. After this, the machine that uses
+the corresponding private SSH key has read-only or read-write (if enabled)
+access to the project.
-You can't add the same deploy key twice with the 'New Deploy Key' option.
+You can't add the same deploy key twice using the form.
If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
@@ -166,6 +189,18 @@ project.
### Eclipse
-How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
+How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
+
+## Troubleshooting
+
+If on Git clone you are prompted for a password like `git@gitlab.com's password:`
+something is wrong with your SSH setup.
+
+- Ensure that you generated your SSH key pair correctly and added the public SSH
+ key to your GitLab profile
+- Try manually registering your private SSH key using `ssh-agent` as documented
+ earlier in this document
+- Try to debug the connection by running `ssh -Tv git@example.com`
+ (replacing `example.com` with your GitLab domain)
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index ec13c2446ef..ad5ffc84473 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -313,8 +313,19 @@ X-Gitlab-Event: System Hook
"git_ssh_url":"git@example.com:mike/diaspora.git",
"visibility_level":0
},
- "commits": [],
- "total_commits_count": 0
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
}
```
diff --git a/doc/university/README.md b/doc/university/README.md
index 8d4e7eff115..c1661f0b52b 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -165,7 +165,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.4. Large Files
-1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+1. [Big files in Git (Git LFS) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
#### 3.5. LDAP and Active Directory
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index ca538ef6dc3..567dadb3b47 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -167,7 +167,6 @@ Some tickets need specific knowledge or a deep understanding of a particular com
Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
-- Set up and try [Git Annex](https://docs.gitlab.com/ee/workflow/git_annex.html)
- Set up and try [Git LFS](https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
- Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 75956aeb360..ed0e668d854 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.6
+sudo -u git -H git checkout v3.6.7
```
### 6. Update gitlab-workhorse
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 954109ba18f..74ffe0bc846 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -139,7 +139,7 @@ sudo -u git -H git checkout v4.1.1
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+There might be new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
@@ -195,6 +195,16 @@ See [smtp_settings.rb.sample] as an example.
#### Init script
+There might be new configuration options available for [`gitlab.default.example`][gl-example].
+You need to update this file if you want to [enable GitLab Pages][pages-admin].
+View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:lib/support/init.d/gitlab.default.example origin/8-17-stable:lib/support/init.d/gitlab.default.example
+```
+
Ensure you're still up-to-date with the latest init script changes:
```bash
@@ -254,3 +264,5 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example
+[pages-admin]: ../administration/pages/source.md
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
new file mode 100644
index 00000000000..b7ba970031c
--- /dev/null
+++ b/doc/update/8.17-to-9.0.md
@@ -0,0 +1,330 @@
+# From 8.17 to 9.0
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --location https://yarnpkg.com/install.sh | bash -
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-0-stable-ee
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 7. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 8. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v5.0.0
+```
+
+### 9. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example
+```
+
+#### Configuration changes for repository storages
+
+This version introduces a new configuration structure for repository storages.
+Update your current configuration as follows, replacing with your storages names and paths:
+
+**For installations from source**
+
+1. Update your `gitlab.yml`, from
+
+ ```yaml
+ repositories:
+ storages: # You must have at least a 'default' storage path.
+ default: /home/git/repositories
+ nfs: /mnt/nfs/repositories
+ cephfs: /mnt/cephfs/repositories
+ ```
+
+ to
+
+ ```yaml
+ repositories:
+ storages: # You must have at least a 'default' storage path.
+ default:
+ path: /home/git/repositories
+ nfs:
+ path: /mnt/nfs/repositories
+ cephfs:
+ path: /mnt/cephfs/repositories
+ ```
+
+**For Omnibus installations**
+
+1. Update your `/etc/gitlab/gitlab.rb`, from
+
+ ```ruby
+ git_data_dirs({
+ "default" => "/var/opt/gitlab/git-data",
+ "nfs" => "/mnt/nfs/git-data",
+ "cephfs" => "/mnt/cephfs/git-data"
+ })
+ ```
+
+ to
+
+ ```ruby
+ git_data_dirs({
+ "default" => { "path" => "/var/opt/gitlab/git-data" },
+ "nfs" => { "path" => "/mnt/nfs/git-data" },
+ "cephfs" => { "path" => "/mnt/cephfs/git-data" }
+ })
+ ```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-17-stable:lib/support/nginx/gitlab-ssl origin/9-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-17-stable:lib/support/nginx/gitlab origin/9-0-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-17-stable:lib/support/init.d/gitlab.default.example origin/9-0-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 10. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 11. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.17)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index b8d24cb2d3b..eb6f915f3f4 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -3,18 +3,36 @@
## Maximum artifacts size
The maximum size of the [job artifacts][art-yml] can be set in the Admin area
-of your GitLab instance. The value is in MB and the default is 100MB. Note that
-this setting is set for each job.
+of your GitLab instance. The value is in *MB* and the default is 100MB. Note
+that this setting is set for each job.
1. Go to **Admin area > Settings** (`/admin/application_settings`).
![Admin area settings button](img/admin_area_settings_button.png)
-1. Change the value of the maximum artifacts size (in MB):
+1. Change the value of maximum artifacts size (in MB):
![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png)
1. Hit **Save** for the changes to take effect.
+## Default artifacts expiration
+
+The default expiration time of the [job artifacts][art-yml] can be set in
+the Admin area of your GitLab instance. The syntax of duration is described
+in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that
+this setting is set for each job. Set it to 0 if you don't want default
+expiration.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+
+ ![Admin area settings button](img/admin_area_settings_button.png)
+
+1. Change the value of default expiration time ([syntax][duration-syntax]):
+
+ ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png)
+
+1. Hit **Save** for the changes to take effect.
[art-yml]: ../../../administration/job_artifacts.md
+[duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in
diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
new file mode 100644
index 00000000000..50a86ede56b
--- /dev/null
+++ b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
index b7d6671902a..33fd29e2039 100644
--- a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
+++ b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
Binary files differ
diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md
new file mode 100644
index 00000000000..acbd2a66d37
--- /dev/null
+++ b/doc/user/award_emojis.md
@@ -0,0 +1,51 @@
+# Award emoji
+
+>**Notes:**
+- First [introduced][1825] in GitLab 8.2.
+- GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform
+ supports them and falls back to images or CSS sprites. This change greatly
+ improved the award emoji performance overall.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and
+virtually everywhere where you can have a discussion.
+
+![Award emoji](img/award_emoji_select.png)
+
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
+
+## Sort issues and merge requests on vote count
+
+> [Introduced][2871] in GitLab 8.5.
+
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+> [Introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown. If you want to remove an award emoji, just click
+the emoji again and the vote will be removed.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570
diff --git a/doc/user/group/subgroups/img/create_new_group.png b/doc/user/group/subgroups/img/create_new_group.png
new file mode 100644
index 00000000000..9d011ec709a
--- /dev/null
+++ b/doc/user/group/subgroups/img/create_new_group.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png
new file mode 100644
index 00000000000..000b54c2855
--- /dev/null
+++ b/doc/user/group/subgroups/img/create_subgroup_button.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/group_members.png b/doc/user/group/subgroups/img/group_members.png
new file mode 100644
index 00000000000..b95fe6263bf
--- /dev/null
+++ b/doc/user/group/subgroups/img/group_members.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/mention_subgroups.png b/doc/user/group/subgroups/img/mention_subgroups.png
new file mode 100644
index 00000000000..8e6bed0111b
--- /dev/null
+++ b/doc/user/group/subgroups/img/mention_subgroups.png
Binary files differ
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
new file mode 100644
index 00000000000..ce5da07c61a
--- /dev/null
+++ b/doc/user/group/subgroups/index.md
@@ -0,0 +1,164 @@
+# Subgroups
+
+> [Introduced][ce-2772] in GitLab 9.0.
+
+With subgroups (aka nested groups or hierarchical groups) you can have
+up to 20 levels of nested groups, which among other things can help you to:
+
+- **Separate internal / external organizations.** Since every group
+ can have its own visibility level, you are able to host groups for different
+ purposes under the same umbrella.
+- **Organize large projects.** For large projects, subgroups makes it
+ potentially easier to separate permissions on parts of the source code.
+- **Make it easier to manage people and control visibility.** Give people
+ different [permissions][] depending on their group [membership](#membership).
+
+## Overview
+
+A group can have many subgroups inside it, and at the same time a group can have
+only 1 parent group. It resembles a directory behavior or a nested items list:
+
+- Group 1
+ - Group 1.1
+ - Group 1.2
+ - Group 1.2.1
+ - Group 1.2.2
+ - Group 1.2.2.1
+
+In a real world example, imagine maintaining a GNU/Linux distribution with the
+first group being the name of the distro and subsequent groups split like:
+
+- Organization Group - GNU/Linux distro
+ - Category Subgroup - Packages
+ - (project) Package01
+ - (project) Package02
+ - Category Subgroup - Software
+ - (project) Core
+ - (project) CLI
+ - (project) Android app
+ - (project) iOS app
+ - Category Subgroup - Infra tools
+ - (project) Ansible playbooks
+
+Another example of GitLab as a company would be the following:
+
+- Organization Group - GitLab
+ - Category Subroup - Marketing
+ - (project) Design
+ - (project) General
+ - Category Subgroup - Software
+ - (project) GitLab CE
+ - (project) GitLab EE
+ - (project) Omnibus GitLab
+ - (project) GitLab Runner
+ - (project) GitLab Pages daemon
+ - Category Subgroup - Infra tools
+ - (project) Chef cookbooks
+ - Category Subgroup - Executive team
+
+---
+
+The maximum nested groups a group can have, including the first one in the
+hierarchy, is 21.
+
+Things like transferring or importing a project inside nested groups, work like
+when performing these actions the traditional way with the `group/project`
+structure.
+
+## Creating a subgroup
+
+>**Notes:**
+- You need to be an Owner of a group in order to be able to create
+ a subgroup. For more information check the [permissions table][permissions].
+- For a list of words that are not allowed to be used as group names see the
+ [`namespace_validator.rb` file][reserved] under the `RESERVED` and
+ `WILDCARD_ROUTES` lists.
+
+To create a subgroup:
+
+1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**.
+
+ ![Subgroups page](img/create_subgroup_button.png)
+
+1. Create a new group like you would normally do. Notice that the parent group
+ namespace is fixed under **Group path**. The visibility level can differ from
+ the parent group.
+
+ ![Subgroups page](img/create_new_group.png)
+
+1. Click the **Create group** button and you will be taken to the new group's
+ dashboard page.
+
+---
+
+You can follow the same process to create any subsequent groups.
+
+## Membership
+
+When you add a member to a subgroup, they inherit the membership and permission
+level from the parent group. This model allows access to nested groups if you
+have membership in one of its parents.
+
+The group permissions for a member can be changed only by Owners and only on
+the **Members** page of the group the member was added.
+
+You can tell if a member has inherited the permissions from a parent group by
+looking at the group's **Members** page.
+
+![Group members page](img/group_members.png)
+
+From the image above, we can deduct the following things:
+
+- There are 5 members that have access to the group `four`
+- User0 is a Reporter and has inherited their permissions from group `one`
+ which is above the hierarchy of group `four`
+- User1 is a Developer and has inherited their permissions from group
+ `one/two` which is above the hierarchy of group `four`
+- User2 is a Developer and has inherited their permissions from group
+ `one/two/three` which is above the hierarchy of group `four`
+- For User3 there is no indication of a parent group, therefore they belong to
+ group `four`, the one we're inspecting
+- Administrator is the Owner and member of **all** subgroups and for that reason,
+ same as User3, there is no indication of an ancestor group
+
+### Overriding the ancestor group membership
+
+>**Note:**
+You need to be an Owner of a group in order to be able to add members to it.
+
+To override a user's membership of an ancestor group (the first group they were
+added to), simply add the user in the new subgroup again, but with different
+permissions.
+
+For example, if User0 was first added to group `group-1/group-1-1` with Developer
+permissions, then they will inherit those permissions in every other subgroup
+of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`,
+you would add them again in that group as Master. Removing them from that group,
+the permissions will fallback to those of the ancestor group.
+
+## Mentioning subgroups
+
+Mentioning groups (`@group`) in issues, commits and merge requests, would
+notify all members of that group. Now with subgroups, there is a more granular
+support if you want to split your group's structure. Mentioning works as before
+and you can choose the group of people to be notified.
+
+![Mentioning subgroups](img/mention_subgroups.png)
+
+## Limitations
+
+Here's a list of what you can't do with subgroups:
+
+- [GitLab Pages](../../project/pages/index.md) are not currently working for
+ projects hosted under a subgroup. That means that only projects hosted under
+ the first parent group will work.
+- Group level labels don't work in subgroups / sub projects
+- It is not possible to share a project with a group that's an ancestor of
+ the group the project is in. That means you can only share as you walk down
+ the hierarchy. For example, `group/subgroup01/project` **cannot** be shared
+ with `group`, but can be shared with `group/subgroup02` or
+ `group/subgroup01/subgroup03`.
+
+[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
+[permissions]: ../../permissions.md#group
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/user/img/award_emoji_comment_awarded.png
index 111793ebf8a..111793ebf8a 100644
--- a/doc/workflow/img/award_emoji_comment_awarded.png
+++ b/doc/user/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/user/img/award_emoji_comment_picker.png
index 3ad1bab3119..3ad1bab3119 100644
--- a/doc/workflow/img/award_emoji_comment_picker.png
+++ b/doc/user/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/user/img/award_emoji_select.png b/doc/user/img/award_emoji_select.png
new file mode 100644
index 00000000000..496acb29eec
--- /dev/null
+++ b/doc/user/img/award_emoji_select.png
Binary files differ
diff --git a/doc/user/img/award_emoji_votes_sort_options.png b/doc/user/img/award_emoji_votes_sort_options.png
new file mode 100644
index 00000000000..dd84b7f4f64
--- /dev/null
+++ b/doc/user/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c14db17b0e6..97de428d11d 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
-Combined emphasis with **asterisks and _underscores_**.
+Combined emphasis with **_asterisks and underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
@@ -576,7 +576,7 @@ Quote break.
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements.
```no-highlight
<dl>
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b49a244160a..0ea6d01411f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -81,6 +81,7 @@ group.
|-------------------------|-------|----------|-----------|--------|-------|
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit group | | | | | ✓ |
+| Create subgroup | | | | | ✓ |
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index a23ad79ae1d..63a3d3c472e 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -213,5 +213,16 @@ your GitLab server's time is synchronized via a service like NTP. Otherwise,
you may have cases where authorization always fails because of time differences.
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
-[FreeOTP]: https://fedorahosted.org/freeotp/
+[FreeOTP]: https://freeotp.github.io/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
+
+- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from
+multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at
+the time of registration, and cannot be used for other hostnames/FQDNs.
+
+ For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
+
+ - The user logs in via `first.host.xyz` and registers their U2F key.
+ - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds.
+ - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
+ the U2F key has only been registered on `first.host.xyz`.
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index c5b2266ff19..7524e70957f 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -248,4 +248,4 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry
+[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
deleted file mode 100644
index f769dece242..00000000000
--- a/doc/user/project/integrations/builds_emails.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Enabling build emails
-
-By enabling this service, you will be able to receive e-mail notifications about
-the result status of your builds.
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Builds emails** service to configure it.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317..f52acf4ef3b 100644
--- a/doc/user/project/integrations/img/mattermost_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png
new file mode 100644
index 00000000000..1f5a44f8820
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png
new file mode 100644
index 00000000000..e30cba211e6
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_node_name.png b/doc/user/project/integrations/img/prometheus_gcp_node_name.png
new file mode 100644
index 00000000000..ea289431454
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_gcp_node_name.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_service_configuration.png b/doc/user/project/integrations/img/prometheus_service_configuration.png
new file mode 100644
index 00000000000..c7dfe874817
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_service_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_yaml_deploy.png b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
new file mode 100644
index 00000000000..978cd7eaa50
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b..527824fc3eb 100644
--- a/doc/user/project/integrations/img/slack_configuration.png
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index cc67e667472..2a890acde4d 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -49,7 +49,8 @@ GitLab CI build environment:
- `KUBE_URL` - equal to the API URL
- `KUBE_TOKEN`
- `KUBE_NAMESPACE`
-- `KUBE_CA_PEM` - only if a custom CA bundle was specified
+- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data.
+- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data.
## Web terminals
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 09ba9994d3a..3e77823a6aa 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -24,23 +24,22 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
+- Confidential issue
- Merge request
- Note
- Tag push
-- Build
+- Pipeline
- Wiki page
-Bellow each of these event checkboxes, you will have an input field to insert
-which Mattermost channel you want to send that event message, with `#town-square`
-being the default. The hash sign is optional.
+Below each of these event checkboxes, you have an input field to enter
+which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional).
At the end, fill in your Mattermost details:
| Field | Description |
| ----- | ----------- |
-| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
+| **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
-
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md
new file mode 100644
index 00000000000..6aefe5dbded
--- /dev/null
+++ b/doc/user/project/integrations/mock_ci.md
@@ -0,0 +1,13 @@
+# Mock CI Service
+
+**NB: This service is only listed if you are in a development environment!**
+
+To setup the mock CI service server, respond to the following endpoints
+
+- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json`
+ - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }`
+ - If the service returns a 404, it is interpreted as `pending`
+- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}`
+ - Just where the build is linked to, doesn't matter if implemented
+
+For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index a3a163a4c6b..25400633de5 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -32,7 +32,6 @@ Click on the service links to see further configuration instructions and details
| Assembla | Project Management Software (Source Commits Endpoint) |
| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
| Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
@@ -48,9 +47,11 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
new file mode 100644
index 00000000000..676a21e85c4
--- /dev/null
+++ b/doc/user/project/integrations/prometheus.md
@@ -0,0 +1,196 @@
+# Prometheus integration
+
+> [Introduced][ce-8935] in GitLab 9.0.
+
+GitLab offers powerful integration with [Prometheus] for monitoring your apps.
+Metrics are retrieved from the configured Prometheus server, and then displayed
+within the GitLab interface.
+
+Each project can be configured with its own specific Prometheus server, see the
+[configuration](#configuration) section for more details. If you have a single
+Prometheus server which monitors all of your infrastructure, you can pre-fill
+the settings page with a default template. To configure the template, see the
+[Services templates](services_templates.md) document.
+
+## Requirements
+
+Integration with Prometheus requires the following:
+
+1. GitLab 9.0 or higher
+1. Your app must be deployed on [Kubernetes][]
+1. Prometheus must be configured to collect Kubernetes metrics
+1. Each metric must be have a label to indicate the environment
+1. GitLab must have network connectivity to the Prometheus sever
+
+There are a few steps necessary to set up integration between Prometheus and
+GitLab.
+
+## Configuring Prometheus to collect Kubernetes metrics
+
+In order for Prometheus to collect Kubernetes metrics, you first must have a
+Prometheus server up and running. You have two options here:
+
+- If you installed Omnibus GitLab inside of Kubernetes, you can simply use the
+ [bundled version of Prometheus][promgldocs]. In that case, follow the info in the
+ [Omnibus GitLab section](#configuring-omnibus-gitlab-prometheus-to-monitor-kubernetes)
+ below.
+- If you are using GitLab.com or installed GitLab outside of Kubernetes, you
+ will likely need to run a Prometheus server within the Kubernetes cluster.
+ Once installed, the easiest way to monitor Kubernetes is to simply use
+ Prometheus' support for [Kubernetes Service Discovery][prometheus-k8s-sd].
+ In that case, follow the instructions on
+ [configuring your own Prometheus server within Kubernetes](#configuring-your-own-prometheus-server-within-kubernetes).
+
+### Configuring Omnibus GitLab Prometheus to monitor Kubernetes
+
+With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
+version of Prometheus to collect the required metrics.
+
+1. Read how to configure the bundled Prometheus server in the
+ [Administration guide][gitlab-prometheus-k8s-monitor].
+1. Now that Prometheus is configured, proceed on
+ [configuring the Prometheus project service in GitLab](#configuration-in-gitlab).
+
+### Configuring your own Prometheus server within Kubernetes
+
+Setting up and configuring Prometheus within Kubernetes is quick and painless.
+The Prometheus project provides an [official Docker image][prometheus-docker-image]
+which we can use as a starting point.
+
+To get started quickly, we have provided a [sample YML file][prometheus-yml]
+that can be used as a template. This file will create a `prometheus` **Namespace**,
+**Service**, **Deployment**, and **ConfigMap** in Kubernetes. You can upload
+this file to the Kubernetes dashboard using **+ Create** at the top right.
+
+![Deploy Prometheus](img/prometheus_yaml_deploy.png)
+
+Or use `kubectl`:
+
+```bash
+kubectl apply -f path/to/prometheus.yml
+```
+
+Once deployed, you should see the Prometheus service, deployment, and
+pod start within the `prometheus` namespace. The server will begin to collect
+metrics from each Kubernetes Node in the cluster, based on the configuration
+provided in the template.
+
+Since GitLab is not running within Kubernetes, the template provides external
+network access via a `NodePort` running on `30090`. This method allows access
+to be controlled using provider firewall rules, like within Google Compute Engine.
+
+Since a `NodePort` does not automatically have firewall rules created for it,
+one will need to be created manually to allow access. In GCP/GKE, you will want
+to confirm the Node that the Prometheus pod is running on. This can be done
+either by looking at the Pod in the Kubernetes dashboard, or by running:
+
+```bash
+kubectl describe pods -n prometheus
+```
+
+Next on GKE, we need to get the `tag` of the Node or VM Instance, so we can
+create an accurate firewall rule. The easiest way to do this is to go into the
+Google Cloud Platform Compute console and select the VM instance that matches
+the name of the Node gathered from the step above. In this case, the node tag
+needed is `gke-prometheus-demo-5d5ada10-node`. Also make a note of the
+**External IP**, which will be the IP address the Prometheus server is reachable
+on.
+
+![GCP Node Detail](img/prometheus_gcp_node_name.png)
+
+Armed with the proper Node tag, the firewall rule can now be created
+specifically for this node. To create the firewall rule, open the Google Cloud
+Platform Networking console, and select **Firewall Rules**.
+
+Create a new rule:
+
+- Specify the source IP range to match your desired access list, which should
+ include your GitLab server. A sample of GitLab.com's IP address range is
+ available [in this issue][gitlab.com-ip-range], but note that GitLab.com's IPs
+ are subject to change without prior notification.
+- Allowed protocol and port should be `tcp:30090`.
+- The target tags should match the Node tag identified earlier in this step.
+
+![GCP Firewall Rule](img/prometheus_gcp_firewall_rule.png)
+
+---
+
+Now that Prometheus is configured, proceed to
+[configure the Prometheus project service in GitLab](##configuration-in-gitlab).
+
+## Configuration in GitLab
+
+The actual configuration of Prometheus integration within GitLab is very simple.
+All you will need is the DNS or IP address of the Prometheus server you'd like
+to integrate with.
+
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click the **Prometheus** service
+1. Provide the base URL of the your server, for example `http://prometheus.example.com/`.
+ The **Test Settings** button can be used to confirm connectivity from GitLab
+ to the Prometheus server.
+
+![Configure Prometheus Service](img/prometheus_service_configuration.png)
+
+## Metrics and Labels
+
+GitLab retrieves performance data from two metrics, `container_cpu_usage_seconds_total`
+and `container_memory_usage_bytes`. These metrics are collected from the
+Kubernetes pods via Prometheus, and report CPU and Memory utilization of each
+container or Pod running in the cluster.
+
+In order to isolate and only display relevant metrics for a given environment
+however, GitLab needs a method to detect which pods are associated. To do that,
+GitLab will specifically request metrics that have an `environment` tag that
+matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
+
+If you are using [GitLab Auto-Deploy][autodeploy] and one of the methods of
+configuring Prometheus above, the `environment` will be automatically added.
+
+### GitLab Prometheus queries
+
+The queries utilized by GitLab are shown in the following table.
+
+| Metric | Query |
+| ------ | ----- |
+| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` |
+| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
+
+## Monitoring CI/CD Environments
+
+Once configured, GitLab will attempt to retrieve performance metrics for any
+environment which has had a successful deployment. If monitoring data was
+successfully retrieved, a metrics button will appear on the environment's
+detail page.
+
+![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
+
+Clicking on the metrics button will display a new page, showing up to the last
+8 hours of performance data. It may take a minute or two for data to appear
+after initial deployment.
+
+## Troubleshooting
+
+If the metrics button is not appearing, then one of a few issues may be
+occurring:
+
+- GitLab is not able to reach the Prometheus server. A test request can be sent
+ to the Prometheus server from the [Prometheus Service](#configuration-in-gitlab)
+ configuration screen.
+- No successful deployments have occurred to this environment.
+- Prometheus does not have performance data for this environment, or the metrics
+ are not labeled correctly. To test this, connect to the Prometheus server and
+ [run a query](#gitlab-prometheus-queries), replacing `$CI_ENVIRONMENT_SLUG`
+ with the name of your environment.
+
+[autodeploy]: ../../../ci/autodeploy/index.md
+[kubernetes]: https://kubernetes.io
+[prometheus-k8s-sd]: https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>
+[prometheus]: https://prometheus.io
+[gitlab-prometheus-k8s-monitor]: ../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes
+[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
+[prometheus-yml]:samples/prometheus.yml
+[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
+[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
+[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
+[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/integrations/samples/prometheus.yml b/doc/user/project/integrations/samples/prometheus.yml
new file mode 100644
index 00000000000..01bbcaffe1e
--- /dev/null
+++ b/doc/user/project/integrations/samples/prometheus.yml
@@ -0,0 +1,69 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: prometheus
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: prometheus
+ namespace: prometheus
+data:
+ prometheus.yml: |-
+ scrape_configs:
+ - job_name: 'kubernetes-nodes'
+ scheme: https
+ tls_config:
+ ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+ insecure_skip_verify: true
+ bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+ kubernetes_sd_configs:
+ - role: node
+ metric_relabel_configs:
+ - source_labels: [pod_name]
+ target_label: environment
+ regex: (.+)-.+-.+
+ replacement: $1
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: prometheus
+ namespace: prometheus
+spec:
+ selector:
+ app: prometheus
+ ports:
+ - name: prometheus
+ protocol: TCP
+ port: 9090
+ nodePort: 30090
+ type: NodePort
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: prometheus
+ namespace: prometheus
+spec:
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: prometheus
+ spec:
+ containers:
+ - name: prometheus
+ image: prom/prometheus:latest
+ args:
+ - '-config.file=/prometheus-data/prometheus.yml'
+ ports:
+ - name: prometheus
+ containerPort: 9090
+ volumeMounts:
+ - name: data-volume
+ mountPath: /prometheus-data
+ volumes:
+ - name: data-volume
+ configMap:
+ name: prometheus
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index 57a9492044b..e8b238351ca 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -21,23 +21,23 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
+- Confidential issue
- Merge request
- Note
- Tag push
-- Build
+- Pipeline
- Wiki page
-Bellow each of these event checkboxes, you will have an input field to insert
-which Slack channel you want to send that event message, with `#general`
-being the default. Enter your preferred channel **without** the hash sign (`#`).
+Below each of these event checkboxes, you have an input field to enter
+which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
At the end, fill in your Slack details:
| Field | Description |
| ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
-| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ed1e867f5fb..dbdc93a77a8 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -250,7 +250,19 @@ X-Gitlab-Event: Issue Hook
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
+ },
+ "labels": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
}
```
### Comment events
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 3199d370a58..5aa8337b75d 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -28,7 +28,7 @@ Below is a table of the definitions used for GitLab's Issue Board.
| -------------- | ------------- |
| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
-| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
+| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. |
There are two types of lists, the ones you create based on your labels, and
one default:
@@ -45,6 +45,7 @@ In short, here's a list of actions you can take in an Issue Board:
- [Create a new list](#creating-a-new-list).
- [Delete an existing list](#deleting-a-list).
- Drag issues between lists.
+- Re-order issues in lists.
- Drag and reorder the lists themselves.
- Change issue labels on-the-fly while dragging issues between lists.
- Close an issue if you drag it to the **Done** list.
@@ -114,6 +115,13 @@ board itself.
![Remove issue from list](img/issue_boards_remove_issue.png)
+## Re-ordering an issue in a list
+
+> Introduced in GitLab 9.0.
+
+Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
+an issue into the order you want.
+
## Filtering issues
You should be able to use the filters on top of your Issue Board to show only
@@ -176,7 +184,6 @@ A few things to remember:
- Clicking on the issue title inside a card will take you to that issue.
- Clicking on a label inside a card will quickly filter the entire Issue Board
and show only the issues from all lists that have that label.
-- Issues inside lists are [ordered by priority][label-priority].
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues start scrolling down and the next
20 will appear.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index cf1d9cbe69c..8ec7adad172 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -65,7 +65,7 @@ issues and merge requests assigned to each label.
> https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
Prioritized labels are like any other label, but sorted by priority. This allows
-you to sort issues and merge requests by priority.
+you to sort issues and merge requests by label priority.
To prioritize labels, navigate to your project's **Issues > Labels** and click
on the star icon next to them to put them in the priority list. Click on the
@@ -77,9 +77,13 @@ having their priority set to null.
![Prioritize labels](img/labels_prioritize.png)
-Now that you have labels prioritized, you can use the 'Priority' filter in the
-issues or merge requests tracker. Those with the highest priority label, will
-appear on top.
+Now that you have labels prioritized, you can use the 'Priority' and 'Label
+priority' filters in the issues or merge requests tracker.
+
+The 'Label priority' filter puts issues with the highest priority label on top.
+
+The 'Priority' filter sorts issues by their soonest milestone due date, then by
+label priority.
![Filter labels by priority](img/labels_filter_by_priority.png)
@@ -156,4 +160,3 @@ mouse over the label in the issue tracker or wherever else the label is
rendered.
![Label tooltips](img/labels_description_tooltip.png)
-
diff --git a/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
new file mode 100644
index 00000000000..b15447ec290
--- /dev/null
+++ b/doc/user/project/merge_requests/img/btn_new_issue_for_all_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
deleted file mode 100644
index f50a1be24f2..00000000000
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
deleted file mode 100644
index ddc58ff2630..00000000000
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
deleted file mode 100644
index a98636ee359..00000000000
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
new file mode 100644
index 00000000000..33f5a4a7a02
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png
index c43f76b058c..c43f76b058c 100644
--- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
new file mode 100644
index 00000000000..9629ed99838
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png
new file mode 100644
index 00000000000..d0691437c65
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/new_issue_for_discussion.png b/doc/user/project/merge_requests/img/new_issue_for_discussion.png
new file mode 100644
index 00000000000..93c9dad8921
--- /dev/null
+++ b/doc/user/project/merge_requests/img/new_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussion.png b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
new file mode 100644
index 00000000000..2ee0653b2ba
--- /dev/null
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussion.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
index 9fdd387676c..3fe0a666678 100644
--- a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
index 8c7ce215ae0..e0ee6a39ffd 100644
--- a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
+++ b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index d4b85676d19..230e957f045 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -53,12 +53,18 @@ are resolved.
## Move all unresolved discussions in a merge request to an issue
-> [Introduced][ce-7180] in GitLab 8.15.
+> [Introduced][ce-8266]
-To delegate unresolved discussions to a new issue you can click the link **open
-an issue to resolve them later**.
+To continue all open discussions in a merge request, click the button **Resolve
+all discussions in new issue**
-![Open new issue from unresolved discussions](img/resolve_discussion_open_issue.png)
+![Open new issue for all unresolved discussions](img/btn_new_issue_for_all_discussions.png)
+
+Alternatively, when your project only accepts merge requests when all discussions
+are resolved, there will be an **open an issue to resolve them later** link in
+the merge request-widget.
+
+![Link in merge request widget](img/resolve_discussion_open_issue.png)
This will prepare an issue with content referring to the merge request and
discussions.
@@ -72,9 +78,28 @@ add a note referring to the newly created issue.
You can now proceed to merge the merge request from the UI.
+## Moving a single discussion to a new issue
+
+> [Introduced][ce-8266]
+
+To create a new issue for a single discussion, you can use the **Resolve this
+discussion in a new issue** button.
+
+![Create issue for discussion](img/new_issue_for_discussion.png)
+
+This will direct you to a new issue prefilled with the content of the
+discussion, similar to the issues created for delegating multiple
+discussions at once.
+
+![New issue for a single discussion](img/preview_issue_for_discussion.png)
+
+Saving the issue will mark the discussion as resolved and add a note
+to the discussion referencing the new issue.
+
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
+[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index c63a408505f..bdd7d0022e6 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -5,7 +5,7 @@ more CI jobs running, you can set it to be merged automatically when the
jobs pipeline succeeds. This way, you don't have to wait for the jobs to
finish and remember to merge the request manually.
-![Enable](img/merge_when_build_succeeds_enable.png)
+![Enable](img/merge_when_pipeline_succeeds_enable.png)
When you hit the "Merge When Pipeline Succeeds" button, the status of the merge
request will be updated to represent the impending merge. If you cannot wait
@@ -16,7 +16,7 @@ Both team developers and the author of the merge request have the option to
cancel the automatic merge if they find a reason why it shouldn't be merged
after all.
-![Status](img/merge_when_build_succeeds_status.png)
+![Status](img/merge_when_pipeline_succeeds_status.png)
When the pipeline succeeds, the merge request will automatically be merged.
When the pipeline fails, the author gets a chance to retry any failed jobs,
@@ -32,15 +32,16 @@ changes to be reviewed.
> **Note:**
You need to have jobs configured to enable this feature.
-You can prevent merge requests from being merged if their pipeline did not succeed.
+You can prevent merge requests from being merged if their pipeline did not succeed
+or if there are discussions to be resolved.
Navigate to your project's settings page, select the
**Only allow merge requests to be merged if the pipeline succeeds** check box and
hit **Save** for the changes to take effect.
-![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
+![Only allow merge if pipeline succeeds settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
From now on, every time the pipeline fails you will not be able to merge the
merge request from the UI, until you make all relevant jobs pass.
-![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
+![Only allow merge if pipeline succeeds message](img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 5f631f63050..b559d132590 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -119,7 +119,7 @@ And then the users could also use it in their CI jobs all Docker related
commands to interact with GitLab Container Registry. For example:
```
-docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
```
Using single token had multiple security implications:
@@ -208,7 +208,7 @@ This is how an example usage can look like:
```
test:
script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker pull $CI_REGISTRY/group/other-project:latest
- docker run $CI_REGISTRY/group/other-project:latest
```
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
new file mode 100644
index 00000000000..35af48724f2
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -0,0 +1,385 @@
+# GitLab Pages from A to Z: Part 4
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- **Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages**
+
+## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages
+
+[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves
+numerous purposes, to build, test, and deploy your app
+from GitLab through
+[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+methods. You will need it to build your website with GitLab Pages,
+and deploy it to the Pages server.
+
+What this file actually does is telling the
+[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts
+as you would do from the command line. The Runner acts as your
+terminal. GitLab CI tells the Runner which commands to run.
+Both are built-in in GitLab, and you don't need to set up
+anything for them to work.
+
+Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+and GitLab Runner is out of the scope of this guide, but we'll
+need to understand just a few things to be able to write our own
+`.gitlab-ci.yml` or tweak an existing one. It's an
+[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file,
+with its own syntax. You can always check your CI syntax with
+the [GitLab CI Lint Tool](https://gitlab.com/ci/lint).
+
+**Practical Example:**
+
+Let's consider you have a [Jekyll](https://jekyllrb.com/) site.
+To build it locally, you would open your terminal, and run `jekyll build`.
+Of course, before building it, you had to install Jekyll in your computer.
+For that, you had to open your terminal and run `gem install jekyll`.
+Right? GitLab CI + GitLab Runner do the same thing. But you need to
+write in the `.gitlab-ci.yml` the script you want to run so
+GitLab Runner will do it for you. It looks more complicated then it
+is. What you need to tell the Runner:
+
+```
+$ gem install jekyll
+$ jekyll build
+```
+
+### Script
+
+To transpose this script to Yaml, it would be like this:
+
+```yaml
+script:
+ - gem install jekyll
+ - jekyll build
+```
+
+### Job
+
+So far so good. Now, each `script`, in GitLab is organized by
+a `job`, which is a bunch of scripts and settings you want to
+apply to that specific task.
+
+```yaml
+job:
+ script:
+ - gem install jekyll
+ - jekyll build
+```
+
+For GitLab Pages, this `job` has a specific name, called `pages`,
+which tells the Runner you want that task to deploy your website
+with GitLab Pages:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build
+```
+
+### The `public` directory
+
+We also need to tell Jekyll where do you want the website to build,
+and GitLab Pages will only consider files in a directory called `public`.
+To do that with Jekyll, we need to add a flag specifying the
+[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the
+built website: `jekyll build -d public`. Of course, we need
+to tell this to our Runner:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+```
+
+### Artifacts
+
+We also need to tell the Runner that this _job_ generates
+_artifacts_, which is the site built by Jekyll.
+Where are these artifacts stored? In the `public` directory:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+The script above would be enough to build your Jekyll
+site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
+template originated by `jekyll new project` requires
+[Bundler](http://bundler.io/) to install Jekyll dependencies
+and the default theme. To adjust our script to meet these new
+requirements, we only need to install and build Jekyll with Bundler:
+
+```yaml
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+That's it! A `.gitlab-ci.yml` with the content above would deploy
+your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
+configuration for our example. On the steps below, we'll refine
+the script by adding extra options to our GitLab CI.
+
+Artifacts will be automatically deleted once GitLab Pages got deployed.
+You can preserve artifacts for limited time by specifying the expiry time.
+
+### Image
+
+At this point, you probably ask yourself: "okay, but to install Jekyll
+I need Ruby. Where is Ruby on that script?". The answer is simple: the
+first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a
+[Docker](https://www.docker.com/) image specifying what do you need in
+your container to run that script:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+In this case, you're telling the Runner to pull this image, which
+contains Ruby 2.3 as part of its file system. When you don't specify
+this image in your configuration, the Runner will use a default
+image, which is Ruby 2.1.
+
+If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll
+need to specify which image you want to use, and this image should
+contain NodeJS as part of its file system. E.g., for a
+[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`.
+
+>**Note:**
+We're not trying to explain what a Docker image is,
+we just need to introduce the concept with a minimum viable
+explanation. To know more about Docker images, please visit
+their website or take a look at a
+[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here.
+
+Let's go a little further.
+
+### Branching
+
+If you use GitLab as a version control platform, you will have your
+branching strategy to work on your project. Meaning, you will have
+other branches in your project, but you'll want only pushes to the
+default branch (usually `master`) to be deployed to your website.
+To do that, we need to add another line to our CI, telling the Runner
+to only perform that _job_ called `pages` on the `master` branch `only`:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+### Stages
+
+Another interesting concept to keep in mind are build stages.
+Your web app can pass through a lot of tests and other tasks
+until it's deployed to staging or production environments.
+There are three default stages on GitLab CI: build, test,
+and deploy. To specify which stage your _job_ is running,
+simply add another line to your CI:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ stage: deploy
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+You might ask yourself: "why should I bother with stages
+at all?" Well, let's say you want to be able to test your
+script and check the built site before deploying your site
+to production. You want to run the test exactly as your
+script will do when you push to `master`. It's simple,
+let's add another task (_job_) to our CI, telling it to
+test every push to other branches, `except` the `master` branch:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ stage: deploy
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle install
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+The `test` job is running on the stage `test`, Jekyll
+will build the site in a directory called `test`, and
+this job will affect all the branches except `master`.
+
+The best benefit of applying _stages_ to different
+_jobs_ is that every job in the same stage builds in
+parallel. So, if your web app needs more than one test
+before being deployed, you can run all your test at the
+same time, it's not necessary to wait one test to finish
+to run the other. Of course, this is just a brief
+introduction of GitLab CI and GitLab Runner, which are
+tools much more powerful than that. This is what you
+need to be able to create and tweak your builds for
+your GitLab Pages site.
+
+### Before Script
+
+To avoid running the same script multiple times across
+your _jobs_, you can add the parameter `before_script`,
+in which you specify which commands you want to run for
+every single _job_. In our example, notice that we run
+`bundle install` for both jobs, `pages` and `test`.
+We don't need to repeat it:
+
+```yaml
+image: ruby:2.3
+
+before_script:
+ - bundle install
+
+pages:
+ stage: deploy
+ script:
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+### Caching Dependencies
+
+If you want to cache the installation files for your
+projects dependencies, for building faster, you can
+use the parameter `cache`. For this example, we'll
+cache Jekyll dependencies in a `vendor` directory
+when we run `bundle install`:
+
+```yaml
+image: ruby:2.3
+
+cache:
+ paths:
+ - vendor/
+
+before_script:
+ - bundle install --path vendor
+
+pages:
+ stage: deploy
+ script:
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+For this specific case, we need to exclude `/vendor`
+from Jekyll `_config.yml` file, otherwise Jekyll will
+understand it as a regular directory to build
+together with the site:
+
+```yml
+exclude:
+ - vendor
+```
+
+There we go! Now our GitLab CI not only builds our website,
+but also **continuously test** pushes to feature-branches,
+**caches** dependencies installed with Bundler, and
+**continuously deploy** every push to the `master` branch.
+
+## Advanced GitLab CI for GitLab Pages
+
+What you can do with GitLab CI is pretty much up to your
+creativity. Once you get used to it, you start creating
+awesome scripts that automate most of tasks you'd do
+manually in the past. Read through the
+[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+to understand how to go even further on your scripts.
+
+- On this blog post, understand the concept of
+[using GitLab CI `environments` to deploy your
+web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/).
+- On this post, learn [how to run jobs sequentially,
+in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+- On this blog post, we go through the process of
+[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+to deploy this website you're looking at, docs.gitlab.com.
+- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+
+|||
+|:--|--:|
+|[**← Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates**](getting_started_part_three.md)||
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
new file mode 100644
index 00000000000..582a4afbab4
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -0,0 +1,106 @@
+# GitLab Pages from A to Z: Part 1
+
+- **Part 1: Static sites and GitLab Pages domains**
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## GitLab Pages form A to Z
+
+This is a comprehensive guide, made for those who want to
+publish a website with GitLab Pages but aren't familiar with
+the entire process involved.
+
+This [first part](#what-you-need-to-know-before-getting-started) of this series will present you to the concepts of
+static sites, and go over how the default Pages domains work.
+
+The [second part](getting_started_part_two.md) covers how to get started with GitLab Pages: deploy
+a website from a forked project or create a new one from scratch.
+
+The [third part](getting_started_part_three.md) will show you how to set up a custom domain or subdomain
+to your site already deployed.
+
+The [fourth part](getting_started_part_four.md) will show you how to create and tweak GitLab CI for
+GitLab Pages.
+
+To **enable** GitLab Pages for GitLab CE (Community Edition)
+and GitLab EE (Enterprise Edition), please read the
+[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html),
+and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s).
+
+>**Note:**
+For this guide, we assume you already have GitLab Pages
+server up and running for your GitLab instance.
+
+## What you need to know before getting started
+
+Before we begin, let's understand a few concepts first.
+
+### Static sites
+
+GitLab Pages only supports static websites, meaning,
+your output files must be HTML, CSS, and JavaScript only.
+
+To create your static site, you can either hardcode in HTML,
+CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/)
+to simplify your code and build the static site for you,
+which is highly recommendable and much faster than hardcoding.
+
+#### Further Reading
+
+- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
+- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- Fork an [example project](https://gitlab.com/pages) to build your website based upon
+
+### GitLab Pages domain
+
+If you set up a GitLab Pages project on GitLab.com,
+it will automatically be accessible under a
+[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/).
+The `namespace` is defined by your username on GitLab.com,
+or the group name you created this project under.
+
+>**Note:**
+If you use your own GitLab instance to deploy your
+site with GitLab Pages, check with your sysadmin what's your
+Pages wildcard domain. This guide is valid for any GitLab instance,
+you just need to replace Pages wildcard domain on GitLab.com
+(`*.gitlab.io`) with your own.
+
+#### Practical examples
+
+**Project Websites:**
+
+- You created a project called `blog` under your username `john`,
+therefore your project URL is `https://gitlab.com/john/blog/`.
+Once you enable GitLab Pages for this project, and build your site,
+it will be available under `https://john.gitlab.io/blog/`.
+- You created a group for all your websites called `websites`,
+and a project within this group is called `blog`. Your project
+URL is `https://gitlab.com/websites/blog/`. Once you enable
+GitLab Pages for this project, the site will live under
+`https://websites.gitlab.io/blog/`.
+
+**User and Group Websites:**
+
+- Under your username, `john`, you created a project called
+`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`.
+Once you enable GitLab Pages for your project, your website
+will be published under `https://john.gitlab.io`.
+- Under your group `websites`, you created a project called
+`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project,
+your website will be published under `https://websites.gitlab.io`.
+
+**General example:**
+
+- On GitLab.com, a project site will always be available under
+`https://namespace.gitlab.io/project-name`
+- On GitLab.com, a user or group website will be available under
+`https://namespace.gitlab.io/`
+- On your GitLab instance, replace `gitlab.io` above with your
+Pages server domain. Ask your sysadmin for this information.
+
+|||
+|:--|--:|
+||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)|
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
new file mode 100644
index 00000000000..55fcd5f00f2
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -0,0 +1,190 @@
+# GitLab Pages from A to Z: Part 3
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+- **Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates**
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## Setting Up Custom Domains - DNS Records and SSL/TLS Certificates
+
+As described in the previous part of this series, setting up GitLab Pages with custom domains, and adding SSL/TLS certificates to them, are optional features of GitLab Pages.
+
+These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`.
+
+### DNS Records
+
+A Domain Name System (DNS) web service routes visitors to websites
+by translating domain names (such as `www.example.com`) into the
+numeric IP addresses (such as `192.0.2.1`) that computers use to
+connect to each other.
+
+A DNS record is created to point a (sub)domain to a certain location,
+which can be an IP address or another domain. In case you want to use
+GitLab Pages with your own (sub)domain, you need to access your domain's
+registrar control panel to add a DNS record pointing it back to your
+GitLab Pages site.
+
+Note that **how to** add DNS records depends on which server your domain
+is hosted on. Every control panel has its own place to do it. If you are
+not an admin of your domain, and don't have access to your registrar,
+you'll need to ask for the technical support of your hosting service
+to do it for you.
+
+To help you out, we've gathered some instructions on how to do that
+for the most popular hosting services:
+
+- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html)
+- [Bluehost](https://my.bluehost.com/cgi/help/559)
+- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-)
+- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone)
+- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-)
+- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238)
+- [Hostgator](http://support.hostgator.com/articles/changing-dns-records)
+- [Inmotion hosting](https://my.bluehost.com/cgi/help/559)
+- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain)
+- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx)
+
+If your hosting service is not listed above, you can just try to
+search the web for "how to add dns record on <my hosting service>".
+
+#### DNS A record
+
+In case you want to point a root domain (`example.com`) to your
+GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
+log into your domain's admin control panel and add a DNS `A` record
+pointing your domain to Pages' server IP address. For projects on
+GitLab.com, this IP is `52.167.214.135`. For projects leaving in
+other GitLab instances (CE or EE), please contact your sysadmin
+asking for this information (which IP address is Pages server
+running on your instance).
+
+**Practical Example:**
+
+![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated.png)
+
+#### DNS CNAME record
+
+In case you want to point a subdomain (`hello-world.example.com`)
+to your GitLab Pages site initially deployed to `namespace.gitlab.io`,
+you need to log into your domain's admin control panel and add a DNS
+`CNAME` record pointing your subdomain to your website URL
+(`namespace.gitlab.io`) address.
+
+Notice that, despite it's a user or project website, the `CNAME`
+should point to your Pages domain (`namespace.gitlab.io`),
+without any `/project-name`.
+
+**Practical Example:**
+
+![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png)
+
+#### TL;DR
+
+| From | DNS Record | To |
+| ---- | ---------- | -- |
+| domain.com | A | 52.167.214.135 |
+| subdomain.domain.com | CNAME | namespace.gitlab.io |
+
+> **Notes**:
+>
+> - **Do not** use a CNAME record if you want to point your
+`domain.com` to your GitLab Pages site. Use an `A` record instead.
+> - **Do not** add any special chars after the default Pages
+domain. E.g., **do not** point your `subdomain.domain.com` to
+`namespace.gitlab.io.` or `namespace.gitlab.io/`.
+> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`.
+
+### SSL/TLS Certificates
+
+Every GitLab Pages project on GitLab.com will be available under
+HTTPS for the default Pages domain (`*.gitlab.io`). Once you set
+up your Pages project with your custom (sub)domain, if you want
+it secured by HTTPS, you will have to issue a certificate for that
+(sub)domain and install it on your project.
+
+>**Note:**
+Certificates are NOT required to add to your custom
+(sub)domain on your GitLab Pages project, though they are
+highly recommendable.
+
+The importance of having any website securely served under HTTPS
+is explained on the introductory section of the blog post
+[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview).
+
+The reason why certificates are so important is that they encrypt
+the connection between the **client** (you, me, your visitors)
+and the **server** (where you site lives), through a keychain of
+authentications and validations.
+
+### Issuing Certificates
+
+GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by
+[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority)
+and self-signed certificates. Of course,
+[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate),
+for security reasons and for having browsers trusting your
+site's certificate.
+
+There are several different kinds of certificates, each one
+with certain security level. A static personal website will
+not require the same security level as an online banking web app,
+for instance. There are a couple Certificate Authorities that
+offer free certificates, aiming to make the internet more secure
+to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/),
+which issues certificates trusted by most of browsers, it's open
+source, and free to use. Please read through this tutorial to
+understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/).
+
+With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/),
+which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/).
+Their certs are valid up to 15 years. Read through the tutorial on
+[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
+
+### Adding certificates to your project
+
+Regardless the CA you choose, the steps to add your certificate to
+your Pages project are the same.
+
+#### What do you need
+
+1. A PEM certificate
+1. An intermediate certificate
+1. A public key
+
+![Pages project - adding certificates](img/add_certificate_to_pages.png)
+
+These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**.
+
+#### What's what?
+
+- A PEM certificate is the certificate generated by the CA,
+which needs to be added to the field **Certificate (PEM)**.
+- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is
+the part of the encryption keychain that identifies the CA.
+Usually it's combined with the PEM certificate, but there are
+some cases in which you need to add them manually.
+[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+are one of these cases.
+- A public key is an encrypted key which validates
+your PEM against your domain.
+
+#### Now what?
+
+Now that you hopefully understand why you need all
+of this, it's simple:
+
+- Your PEM certificate needs to be added to the first field
+- If your certificate is missing its intermediate, copy
+and paste the root certificate (usually available from your CA website)
+and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
+just jumping a line between them.
+- Copy your public key and paste it in the last field
+
+>**Note:**
+**Do not** open certificates or encryption keys in
+regular text editors. Always use code editors (such as
+Sublime Text, Atom, Dreamweaver, Brackets, etc).
+
+|||
+|:--|--:|
+|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|[**Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_four.md)|
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
new file mode 100644
index 00000000000..d0e2c467fee
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -0,0 +1,154 @@
+# GitLab Pages from A to Z: Part 2
+
+- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+- **Part 2: Quick start guide - Setting up GitLab Pages**
+- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+
+## Setting up GitLab Pages
+
+For a complete step-by-step tutorial, please read the
+blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain
+what do you need and why do you need them.
+
+## What you need to get started
+
+1. A project
+1. A configuration file (`.gitlab-ci.yml`) to deploy your site
+1. A specific `job` called `pages` in the configuration file
+that will make GitLab aware that you are deploying a GitLab Pages website
+
+Optional Features:
+
+1. A custom domain or subdomain
+1. A DNS pointing your (sub)domain to your Pages site
+ 1. **Optional**: an SSL/TLS certificate so your custom
+ domain is accessible under HTTPS.
+
+The optional settings, custom domain, DNS records, and SSL/TLS certificates, are described in [Part 3](getting_started_part_three.md)).
+
+## Project
+
+Your GitLab Pages project is a regular project created the
+same way you do for the other ones. To get started with GitLab Pages, you have two ways:
+
+- Fork one of the templates from Page Examples, or
+- Create a new project from scratch
+
+Let's go over both options.
+
+### Fork a project to get started from
+
+To make things easy for you, we've created this
+[group](https://gitlab.com/pages) of default projects
+containing the most popular SSGs templates.
+
+Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've
+created for the steps below.
+
+1. Choose your SSG template
+1. Fork a project from the [Pages group](https://gitlab.com/pages)
+1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project**
+
+ ![remove fork relashionship](img/remove_fork_relashionship.png)
+
+1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines**
+1. Trigger a build (push a change to any file)
+1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages**
+
+To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to:
+
+- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository**
+- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file.
+
+> **Notes:**
+>
+>1. Why do I need to remove the fork relationship?
+>
+> Unless you want to contribute to the original project,
+you won't need it connected to the upstream. A
+[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork)
+is useful for submitting merge requests to the upstream.
+>
+> 2. Why do I need to enable Shared Runners?
+>
+> Shared Runners will run the script set by your GitLab CI
+configuration file. They're enabled by default to new projects,
+but not to forks.
+
+### Create a project from scratch
+
+1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
+click **New project**, and name it considering the
+[practical examples](getting_started_part_one.md#practical-examples).
+1. Clone it to your local computer, add your website
+files to your project, add, commit and push to GitLab.
+1. From the your **Project**'s page, click **Set up CI**:
+
+ ![setup GitLab CI](img/setup_ci.png)
+
+1. Choose one of the templates from the dropbox menu.
+Pick up the template corresponding to the SSG you're using (or plain HTML).
+
+ ![gitlab-ci templates](img/choose_ci_template.png)
+
+Once you have both site files and `.gitlab-ci.yml` in your project's
+root, GitLab CI will build your site and deploy it with Pages.
+Once the first build passes, you see your site is live by
+navigating to your **Project**'s **Settings** > **Pages**,
+where you'll find its default URL.
+
+> **Notes:**
+>
+> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
+if you don't find yours among the templates, you'll need
+to configure your own `.gitlab-ci.yml`. Do do that, please
+read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among
+the [example projects](https://gitlab.com/pages). If you set
+up a new one, please
+[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md)
+to our examples.
+>
+> - The second step _"Clone it to your local computer"_, can be done
+differently, achieving the same results: instead of cloning the bare
+repository to you local computer and moving your site files into it,
+you can run `git init` in your local website directory, add the
+remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
+then add, commit, and push.
+
+### URLs and Baseurls
+
+Every Static Site Generator (SSG) default configuration expects
+to find your website under a (sub)domain (`example.com`), not
+in a subdirectory of that domain (`example.com/subdir`). Therefore,
+whenever you publish a project website (`namespace.gitlab.io/project-name`),
+you'll have to look for this configuration (base URL) on your SSG's
+documentation and set it up to reflect this pattern.
+
+For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
+configuration file, `_config.yml`. If your website URL is
+`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
+
+```yaml
+baseurl: "/blog"
+```
+
+On the contrary, if you deploy your website after forking one of
+our [default examples](https://gitlab.com/pages), the baseurl will
+already be configured this way, as all examples there are project
+websites. If you decide to make yours a user or group website, you'll
+have to remove this configuration from your project. For the Jekyll
+example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
+
+```yaml
+baseurl: ""
+```
+
+### Custom Domains
+
+GitLab Pages supports custom domains and subdomains, served under HTTPS or HTTPS.
+Please check the [next part](getting_started_part_three.md) of this series for an overview.
+
+|||
+|:--|--:|
+|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Setting Up Custom Domains - DNS Records and SSL/TLS Certificates →**](getting_started_part_three.md)|
diff --git a/doc/user/project/pages/img/add_certificate_to_pages.png b/doc/user/project/pages/img/add_certificate_to_pages.png
new file mode 100644
index 00000000000..d92a981dc60
--- /dev/null
+++ b/doc/user/project/pages/img/add_certificate_to_pages.png
Binary files differ
diff --git a/doc/user/project/pages/img/choose_ci_template.png b/doc/user/project/pages/img/choose_ci_template.png
new file mode 100644
index 00000000000..0697542abc8
--- /dev/null
+++ b/doc/user/project/pages/img/choose_ci_template.png
Binary files differ
diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png
new file mode 100644
index 00000000000..2661a497b91
--- /dev/null
+++ b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png
Binary files differ
diff --git a/doc/user/project/pages/img/dns_cname_record_example.png b/doc/user/project/pages/img/dns_cname_record_example.png
new file mode 100644
index 00000000000..43d1a838544
--- /dev/null
+++ b/doc/user/project/pages/img/dns_cname_record_example.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
index a936d8e5dbd..be47f9d2a44 100644
--- a/doc/user/project/pages/img/pages_create_project.png
+++ b/doc/user/project/pages/img/pages_create_project.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
index 3f615d3757d..2f1a19ae424 100644
--- a/doc/user/project/pages/img/pages_create_user_page.png
+++ b/doc/user/project/pages/img/pages_create_user_page.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
index 8d34f3b7f38..274e98fde4d 100644
--- a/doc/user/project/pages/img/pages_dns_details.png
+++ b/doc/user/project/pages/img/pages_dns_details.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
index 2bc7cee07a6..6bc92db6b41 100644
--- a/doc/user/project/pages/img/pages_multiple_domains.png
+++ b/doc/user/project/pages/img/pages_multiple_domains.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
index c3640133bb2..cd59defa006 100644
--- a/doc/user/project/pages/img/pages_new_domain_button.png
+++ b/doc/user/project/pages/img/pages_new_domain_button.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png
index adbfb654877..b064310380e 100644
--- a/doc/user/project/pages/img/pages_remove.png
+++ b/doc/user/project/pages/img/pages_remove.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
index 06d85ab1971..dc431ea3fef 100644
--- a/doc/user/project/pages/img/pages_upload_cert.png
+++ b/doc/user/project/pages/img/pages_upload_cert.png
Binary files differ
diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relashionship.png
new file mode 100644
index 00000000000..67c45491f08
--- /dev/null
+++ b/doc/user/project/pages/img/remove_fork_relashionship.png
Binary files differ
diff --git a/doc/user/project/pages/img/setup_ci.png b/doc/user/project/pages/img/setup_ci.png
new file mode 100644
index 00000000000..214c1cc668f
--- /dev/null
+++ b/doc/user/project/pages/img/setup_ci.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 4c4f15aad40..abe6b4cbd8e 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,437 +1,49 @@
-# GitLab Pages
-
-> **Notes:**
-> - This feature was [introduced][ee-80] in GitLab EE 8.3.
-> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
-> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
-> - This document is about the user guide. To learn how to enable GitLab Pages
-> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
-
-With GitLab Pages you can host for free your static websites on GitLab.
-Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
-deploy static pages for your individual projects, your user or your group.
-
-Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
-information, if you are using GitLab.com to host your website.
-
-## Getting started with GitLab Pages
-
-> **Note:**
-> In the rest of this document we will assume that the general domain name that
-> is used for GitLab Pages is `example.io`.
-
-In general there are two types of pages one might create:
-
-- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
-- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
-
-In GitLab, usernames and groupnames are unique and we often refer to them
-as namespaces. There can be only one namespace in a GitLab instance. Below you
-can see the connection between the type of GitLab Pages, what the project name
-that is created on GitLab looks like and the website URL it will be ultimately
-be served on.
-
-| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
-| -------------------- | ------------ | ----------- |
-| User pages | `username.example.io` | `http(s)://username.example.io` |
-| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
-| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` |
-| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
-
-> **Warning:**
-> There are some known [limitations](#limitations) regarding namespaces served
-> under the general domain name and HTTPS. Make sure to read that section.
-
-### GitLab Pages requirements
-
-In brief, this is what you need to upload your website in GitLab Pages:
-
-1. Find out the general domain name that is used for GitLab Pages
- (ask your administrator). This is very important, so you should first make
- sure you get that right.
-1. Create a project
-1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
- of your repository with a specific job named [`pages`][pages]
-1. Set up a GitLab Runner to build your website
-
-> **Note:**
-If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab
-administrator, you should be able to use them instead of bringing your own.
-
-### User or group Pages
-
-For user and group pages, the name of the project should be specific to the
-username or groupname and the general domain name that is used for GitLab Pages.
-Head over your GitLab instance that supports GitLab Pages and create a
-repository named `username.example.io`, where `username` is your username on
-GitLab. If the first part of the project name doesn't match exactly your
-username, it won’t work, so make sure to get it right.
-
-To create a group page, the steps are the same like when creating a website for
-users. Just make sure that you are creating the project within the group's
-namespace.
-
-![Create a user-based pages project](img/pages_create_user_page.png)
-
----
-
-After you push some static content to your repository and GitLab Runner uploads
-the artifacts to GitLab CI, you will be able to access your website under
-`http(s)://username.example.io`. Keep reading to find out how.
-
->**Note:**
-If your username/groupname contains a dot, for example `foo.bar`, you will not
-be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
-
-### Project Pages
-
-GitLab Pages for projects can be created by both user and group accounts.
-The steps to create a project page for a user or a group are identical:
-
-1. Create a new project
-1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
- of your repository with a specific job named [`pages`][pages].
-1. Set up a GitLab Runner to build your website
-
-A user's project will be served under `http(s)://username.example.io/projectname`
-whereas a group's project under `http(s)://groupname.example.io/projectname`.
-
-### Explore the contents of `.gitlab-ci.yml`
-
-The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
-gives you absolute control over the build process. You can actually watch your
-website being built live by following the CI job traces.
-
-> **Note:**
-> Before reading this section, make sure you familiarize yourself with GitLab CI
-> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
-> following our [quick start guide].
-
-To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
-rules below:
-
-1. A special job named [`pages`][pages] must be defined
-1. Any static content which will be served by GitLab Pages must be placed under
- a `public/` directory
-1. `artifacts` with a path to the `public/` directory must be defined
-
-In its simplest form, `.gitlab-ci.yml` looks like:
-
-```yaml
-pages:
- script:
- - my_commands
- artifacts:
- paths:
- - public
-```
-
-When the Runner reaches to build the `pages` job, it executes whatever is
-defined in the `script` parameter and if the job completes with a non-zero
-exit status, it then uploads the `public/` directory to GitLab Pages.
-
-The `public/` directory should contain all the static content of your website.
-Depending on how you plan to publish your website, the steps defined in the
-[`script` parameter](../../../ci/yaml/README.md#script) may differ.
-
-Be aware that Pages are by default branch/tag agnostic and their deployment
-relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
-`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
-whenever a new commit is pushed to whatever branch or tag, the Pages will be
-overwritten. In the example below, we limit the Pages to be deployed whenever
-a commit is pushed only on the `master` branch:
-
-```yaml
-pages:
- script:
- - my_commands
- artifacts:
- paths:
- - public
- only:
- - master
-```
-
-We then tell the Runner to treat the `public/` directory as `artifacts` and
-upload it to GitLab. And since all these parameters were all under a `pages`
-job, the contents of the `public` directory will be served by GitLab Pages.
-
-#### How `.gitlab-ci.yml` looks like when the static content is in your repository
-
-Supposedly your repository contained the following files:
-
-```
-├── index.html
-├── css
-│   └── main.css
-└── js
- └── main.js
-```
-
-Then the `.gitlab-ci.yml` example below simply moves all files from the root
-directory of the project to the `public/` directory. The `.public` workaround
-is so `cp` doesn't also copy `public/` to itself in an infinite loop:
-
-```yaml
-pages:
- script:
- - mkdir .public
- - cp -r * .public
- - mv .public public
- artifacts:
- paths:
- - public
- only:
- - master
-```
-
-#### How `.gitlab-ci.yml` looks like when using a static generator
-
-In general, GitLab Pages support any kind of [static site generator][staticgen],
-since `.gitlab-ci.yml` can be configured to run any possible command.
-
-In the root directory of your Git repository, place the source files of your
-favorite static generator. Then provide a `.gitlab-ci.yml` file which is
-specific to your static generator.
-
-The example below, uses [Jekyll] to build the static site:
-
-```yaml
-image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1
-
-pages: # the build job must be named pages
- script:
- - gem install jekyll # we install jekyll
- - jekyll build -d public/ # we tell jekyll to build the site for us
- artifacts:
- paths:
- - public # this is where the site will live and the Runner uploads it in GitLab
- only:
- - master # this script is only affecting the master branch
-```
-
-Here, we used the Docker executor and in the first line we specified the base
-image against which our jobs will run.
-
-You have to make sure that the generated static files are ultimately placed
-under the `public` directory, that's why in the `script` section we run the
-`jekyll` command that jobs the website and puts all content in the `public/`
-directory. Depending on the static generator of your choice, this command will
-differ. Search in the documentation of the static generator you will use if
-there is an option to explicitly set the output directory. If there is not
-such an option, you can always add one more line under `script` to rename the
-resulting directory in `public/`.
-
-We then tell the Runner to treat the `public/` directory as `artifacts` and
-upload it to GitLab.
-
----
-
-See the [jekyll example project][pages-jekyll] to better understand how this
-works.
-
-For a list of Pages projects, see the [example projects](#example-projects) to
-get you started.
-
-#### How to set up GitLab Pages in a repository where there's also actual code
-
-Remember that GitLab Pages are by default branch/tag agnostic and their
-deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
-the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
-whenever a new commit is pushed to a branch that will be used specifically for
-your pages.
-
-That way, you can have your project's code in the `master` branch and use an
-orphan branch (let's name it `pages`) that will host your static generator site.
-
-You can create a new empty branch like this:
-
-```bash
-git checkout --orphan pages
-```
-
-The first commit made on this new branch will have no parents and it will be
-the root of a new history totally disconnected from all the other branches and
-commits. Push the source files of your static generator in the `pages` branch.
-
-Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
-one, specifying to execute everything in the `pages` branch:
-
-```
-image: ruby:2.1
-
-pages:
- script:
- - gem install jekyll
- - jekyll build -d public/
- artifacts:
- paths:
- - public
- only:
- - pages
-```
-
-See an example that has different files in the [`master` branch][jekyll-master]
-and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
-also includes `.gitlab-ci.yml`.
-
-[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
-[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
-
-## Next steps
-
-So you have successfully deployed your website, congratulations! Let's check
-what more you can do with GitLab Pages.
-
-### Example projects
-
-Below is a list of example projects for GitLab Pages with a plain HTML website
-or various static site generators. Contributions are very welcome.
-
-- [Plain HTML](https://gitlab.com/pages/plain-html)
-- [Jekyll](https://gitlab.com/pages/jekyll)
-- [Hugo](https://gitlab.com/pages/hugo)
-- [Middleman](https://gitlab.com/pages/middleman)
-- [Hexo](https://gitlab.com/pages/hexo)
-- [Brunch](https://gitlab.com/pages/brunch)
-- [Metalsmith](https://gitlab.com/pages/metalsmith)
-- [Harp](https://gitlab.com/pages/harp)
-
-Visit the GitLab Pages group for a full list of example projects:
-<https://gitlab.com/groups/pages>.
-
-### Add a custom domain to your Pages website
-
-If this setting is enabled by your GitLab administrator, you should be able to
-see the **New Domain** button when visiting your project's settings through the
-gear icon in the top right and then navigating to **Pages**.
-
-![New domain button](img/pages_new_domain_button.png)
-
----
-
-You can add multiple domains pointing to your website hosted under GitLab.
-Once the domain is added, you can see it listed under the **Domains** section.
-
-![Pages multiple domains](img/pages_multiple_domains.png)
-
----
-
-As a last step, you need to configure your DNS and add a CNAME pointing to your
-user/group page. Click on the **Details** button of a domain for further
-instructions.
-
-![Pages DNS details](img/pages_dns_details.png)
-
----
-
->**Note:**
-Currently there is support only for custom domains on per-project basis. That
-means that if you add a custom domain (`example.com`) for your user website
-(`username.example.io`), a project that is served under `username.example.io/foo`,
-will not be accessible under `example.com/foo`.
-
-### Secure your custom domain website with TLS
-
-When you add a new custom domain, you also have the chance to add a TLS
-certificate. If this setting is enabled by your GitLab administrator, you
-should be able to see the option to upload the public certificate and the
-private key when adding a new domain.
-
-![Pages upload cert](img/pages_upload_cert.png)
-
-### Custom error codes pages
-
-You can provide your own 403 and 404 error pages by creating the `403.html` and
-`404.html` files respectively in the root directory of the `public/` directory
-that will be included in the artifacts. Usually this is the root directory of
-your project, but that may differ depending on your static generator
-configuration.
-
-If the case of `404.html`, there are different scenarios. For example:
-
-- If you use project Pages (served under `/projectname/`) and try to access
- `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
- `/projectname/404.html`, and then `/404.html`.
-- If you use user/group Pages (served under `/`) and try to access
- `/non/existing_file` GitLab Pages will try to serve `/404.html`.
-- If you use a custom domain and try to access `/non/existing_file`, GitLab
- Pages will try to serve only `/404.html`.
-
-### Remove the contents of your pages
-
-If you ever feel the need to purge your Pages content, you can do so by going
-to your project's settings through the gear icon in the top right, and then
-navigating to **Pages**. Hit the **Remove pages** button and your Pages website
-will be deleted. Simple as that.
-
-![Remove pages](img/pages_remove.png)
-
-## GitLab Pages on GitLab.com
-
-If you are using GitLab.com to host your website, then:
-
-- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
-- Custom domains and TLS support are enabled.
-- Shared runners are enabled by default, provided for free and can be used to
- build your website. If you want you can still bring your own Runner.
-
-The rest of the guide still applies.
-
-## Limitations
-
-When using Pages under the general domain of a GitLab instance (`*.example.io`),
-you _cannot_ use HTTPS with sub-subdomains. That means that if your
-username/groupname contains a dot, for example `foo.bar`, the domain
-`https://foo.bar.example.io` will _not_ work. This is a limitation of the
-[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
-don't redirect HTTP to HTTPS.
-
-[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
-
-## Redirects in GitLab Pages
-
-Since you cannot use any custom server configuration files, like `.htaccess` or
-any `.conf` file for that matter, if you want to redirect a web page to another
-location, you can use the [HTTP meta refresh tag][metarefresh].
-
-Some static site generators provide plugins for that functionality so that you
-don't have to create and edit HTML files manually. For example, Jekyll has the
-[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
-
-## Frequently Asked Questions
-
-### Can I download my generated pages?
-
-Sure. All you need to do is download the artifacts archive from the job page.
-
-### Can I use GitLab Pages if my project is private?
-
-Yes. GitLab Pages don't care whether you set your project's visibility level
-to private, internal or public.
-
-### Do I need to create a user/group website before creating a project website?
-
-No, you don't. You can create your project first and it will be accessed under
-`http(s)://namespace.example.io/projectname`.
-
-## Known issues
-
-For a list of known issues, visit GitLab's [public issue tracker].
-
----
-
-[jekyll]: http://jekyllrb.com/
-[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
-[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
-[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
-[gitlab ci]: https://about.gitlab.com/gitlab-ci
-[gitlab runner]: https://docs.gitlab.com/runner
-[pages]: ../../../ci/yaml/README.md#pages
-[yaml]: ../../../ci/yaml/README.md
-[staticgen]: https://www.staticgen.com/
-[pages-jekyll]: https://gitlab.com/pages/jekyll
-[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
-[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages
-[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
-[quick start guide]: ../../../ci/quick_start/README.md
+# GitLab Pages documentation
+
+With GitLab Pages you can create static websites for your GitLab projects,
+groups, or user accounts. You can use any static website generator: Jekyll,
+Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains
+as you like and bring your own TLS certificate to secure them.
+
+Here's some info we've gathered to get you started.
+
+## General info
+
+- [Product webpage](https://pages.gitlab.io)
+- ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
+- [Pages group - templates](https://gitlab.com/pages)
+- [General user documentation](introduction.md)
+- [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md)
+- ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
+
+## Getting started
+
+- **GitLab Pages from A to Z**
+ - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
+ - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+ - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
+ - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
+- **Static Site Generators - Blog posts series**
+ - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+ - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/)
+ - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- **Secure GitLab Pages custom domain with SSL/TLS certificates**
+ - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/)
+ - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+ - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+- **General**
+ - [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide
+ - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/)
+
+## Video tutorials
+
+- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg)
+- [How to Enable GitLab Pages for GitLab CE and EE (for Admins only)](https://youtu.be/dD8c7WNcc6s)
+
+## Advanced use
+
+- **Blog Posts**
+ - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+ - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+ - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+ - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
new file mode 100644
index 00000000000..deaceabb7c5
--- /dev/null
+++ b/doc/user/project/pages/introduction.md
@@ -0,0 +1,447 @@
+# GitLab Pages
+
+> **Notes:**
+> - This feature was [introduced][ee-80] in GitLab EE 8.3.
+> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - This document is about the user guide. To learn how to enable GitLab Pages
+> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
+
+With GitLab Pages you can host for free your static websites on GitLab.
+Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
+deploy static pages for your individual projects, your user or your group.
+
+Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
+information, if you are using GitLab.com to host your website.
+
+Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials).
+
+## Getting started with GitLab Pages
+
+> **Note:**
+> In the rest of this document we will assume that the general domain name that
+> is used for GitLab Pages is `example.io`.
+
+In general there are two types of pages one might create:
+
+- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
+- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+
+In GitLab, usernames and groupnames are unique and we often refer to them
+as namespaces. There can be only one namespace in a GitLab instance. Below you
+can see the connection between the type of GitLab Pages, what the project name
+that is created on GitLab looks like and the website URL it will be ultimately
+be served on.
+
+| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
+| -------------------- | ------------ | ----------- |
+| User pages | `username.example.io` | `http(s)://username.example.io` |
+| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
+| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` |
+| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
+
+> **Warning:**
+> There are some known [limitations](#limitations) regarding namespaces served
+> under the general domain name and HTTPS. Make sure to read that section.
+
+### GitLab Pages requirements
+
+In brief, this is what you need to upload your website in GitLab Pages:
+
+1. Find out the general domain name that is used for GitLab Pages
+ (ask your administrator). This is very important, so you should first make
+ sure you get that right.
+1. Create a project
+1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
+ of your repository with a specific job named [`pages`][pages]
+1. Set up a GitLab Runner to build your website
+
+> **Note:**
+If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab
+administrator, you should be able to use them instead of bringing your own.
+
+### User or group Pages
+
+For user and group pages, the name of the project should be specific to the
+username or groupname and the general domain name that is used for GitLab Pages.
+Head over your GitLab instance that supports GitLab Pages and create a
+repository named `username.example.io`, where `username` is your username on
+GitLab. If the first part of the project name doesn't match exactly your
+username, it won’t work, so make sure to get it right.
+
+To create a group page, the steps are the same like when creating a website for
+users. Just make sure that you are creating the project within the group's
+namespace.
+
+![Create a user-based pages project](img/pages_create_user_page.png)
+
+---
+
+After you push some static content to your repository and GitLab Runner uploads
+the artifacts to GitLab CI, you will be able to access your website under
+`http(s)://username.example.io`. Keep reading to find out how.
+
+>**Note:**
+If your username/groupname contains a dot, for example `foo.bar`, you will not
+be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
+
+### Project Pages
+
+GitLab Pages for projects can be created by both user and group accounts.
+The steps to create a project page for a user or a group are identical:
+
+1. Create a new project
+1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory
+ of your repository with a specific job named [`pages`][pages].
+1. Set up a GitLab Runner to build your website
+
+A user's project will be served under `http(s)://username.example.io/projectname`
+whereas a group's project under `http(s)://groupname.example.io/projectname`.
+
+## Quick Start
+
+Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on
+[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork].
+
+See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages.
+
+### Explore the contents of `.gitlab-ci.yml`
+
+The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
+gives you absolute control over the build process. You can actually watch your
+website being built live by following the CI job traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
+> following our [quick start guide].
+
+To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
+rules below:
+
+1. A special job named [`pages`][pages] must be defined
+1. Any static content which will be served by GitLab Pages must be placed under
+ a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+In its simplest form, `.gitlab-ci.yml` looks like:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+```
+
+When the Runner reaches to build the `pages` job, it executes whatever is
+defined in the `script` parameter and if the job completes with a non-zero
+exit status, it then uploads the `public/` directory to GitLab Pages.
+
+The `public/` directory should contain all the static content of your website.
+Depending on how you plan to publish your website, the steps defined in the
+[`script` parameter](../../../ci/yaml/README.md#script) may differ.
+
+Be aware that Pages are by default branch/tag agnostic and their deployment
+relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
+`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to whatever branch or tag, the Pages will be
+overwritten. In the example below, we limit the Pages to be deployed whenever
+a commit is pushed only on the `master` branch:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab. And since all these parameters were all under a `pages`
+job, the contents of the `public` directory will be served by GitLab Pages.
+
+#### How `.gitlab-ci.yml` looks like when the static content is in your repository
+
+Supposedly your repository contained the following files:
+
+```
+├── index.html
+├── css
+│   └── main.css
+└── js
+ └── main.js
+```
+
+Then the `.gitlab-ci.yml` example below simply moves all files from the root
+directory of the project to the `public/` directory. The `.public` workaround
+is so `cp` doesn't also copy `public/` to itself in an infinite loop:
+
+```yaml
+pages:
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+#### How `.gitlab-ci.yml` looks like when using a static generator
+
+In general, GitLab Pages support any kind of [static site generator][staticgen],
+since `.gitlab-ci.yml` can be configured to run any possible command.
+
+In the root directory of your Git repository, place the source files of your
+favorite static generator. Then provide a `.gitlab-ci.yml` file which is
+specific to your static generator.
+
+The example below, uses [Jekyll] to build the static site:
+
+```yaml
+image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1
+
+pages: # the build job must be named pages
+ script:
+ - gem install jekyll # we install jekyll
+ - jekyll build -d public/ # we tell jekyll to build the site for us
+ artifacts:
+ paths:
+ - public # this is where the site will live and the Runner uploads it in GitLab
+ only:
+ - master # this script is only affecting the master branch
+```
+
+Here, we used the Docker executor and in the first line we specified the base
+image against which our jobs will run.
+
+You have to make sure that the generated static files are ultimately placed
+under the `public` directory, that's why in the `script` section we run the
+`jekyll` command that jobs the website and puts all content in the `public/`
+directory. Depending on the static generator of your choice, this command will
+differ. Search in the documentation of the static generator you will use if
+there is an option to explicitly set the output directory. If there is not
+such an option, you can always add one more line under `script` to rename the
+resulting directory in `public/`.
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab.
+
+---
+
+See the [jekyll example project][pages-jekyll] to better understand how this
+works.
+
+For a list of Pages projects, see the [example projects](#example-projects) to
+get you started.
+
+#### How to set up GitLab Pages in a repository where there's also actual code
+
+Remember that GitLab Pages are by default branch/tag agnostic and their
+deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
+the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to a branch that will be used specifically for
+your pages.
+
+That way, you can have your project's code in the `master` branch and use an
+orphan branch (let's name it `pages`) that will host your static generator site.
+
+You can create a new empty branch like this:
+
+```bash
+git checkout --orphan pages
+```
+
+The first commit made on this new branch will have no parents and it will be
+the root of a new history totally disconnected from all the other branches and
+commits. Push the source files of your static generator in the `pages` branch.
+
+Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
+one, specifying to execute everything in the `pages` branch:
+
+```
+image: ruby:2.1
+
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - pages
+```
+
+See an example that has different files in the [`master` branch][jekyll-master]
+and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+also includes `.gitlab-ci.yml`.
+
+[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
+[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
+
+## Next steps
+
+So you have successfully deployed your website, congratulations! Let's check
+what more you can do with GitLab Pages.
+
+### Example projects
+
+Below is a list of example projects for GitLab Pages with a plain HTML website
+or various static site generators. Contributions are very welcome.
+
+- [Plain HTML](https://gitlab.com/pages/plain-html)
+- [Jekyll](https://gitlab.com/pages/jekyll)
+- [Hugo](https://gitlab.com/pages/hugo)
+- [Middleman](https://gitlab.com/pages/middleman)
+- [Hexo](https://gitlab.com/pages/hexo)
+- [Brunch](https://gitlab.com/pages/brunch)
+- [Metalsmith](https://gitlab.com/pages/metalsmith)
+- [Harp](https://gitlab.com/pages/harp)
+
+Visit the GitLab Pages group for a full list of example projects:
+<https://gitlab.com/groups/pages>.
+
+### Add a custom domain to your Pages website
+
+If this setting is enabled by your GitLab administrator, you should be able to
+see the **New Domain** button when visiting your project's settings through the
+gear icon in the top right and then navigating to **Pages**.
+
+![New domain button](img/pages_new_domain_button.png)
+
+---
+
+You can add multiple domains pointing to your website hosted under GitLab.
+Once the domain is added, you can see it listed under the **Domains** section.
+
+![Pages multiple domains](img/pages_multiple_domains.png)
+
+---
+
+As a last step, you need to configure your DNS and add a CNAME pointing to your
+user/group page. Click on the **Details** button of a domain for further
+instructions.
+
+![Pages DNS details](img/pages_dns_details.png)
+
+---
+
+>**Note:**
+Currently there is support only for custom domains on per-project basis. That
+means that if you add a custom domain (`example.com`) for your user website
+(`username.example.io`), a project that is served under `username.example.io/foo`,
+will not be accessible under `example.com/foo`.
+
+### Secure your custom domain website with TLS
+
+When you add a new custom domain, you also have the chance to add a TLS
+certificate. If this setting is enabled by your GitLab administrator, you
+should be able to see the option to upload the public certificate and the
+private key when adding a new domain.
+
+![Pages upload cert](img/pages_upload_cert.png)
+
+### Custom error codes pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+ `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
+ `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+ `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+ Pages will try to serve only `/404.html`.
+
+### Remove the contents of your pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted. Simple as that.
+
+![Remove pages](img/pages_remove.png)
+
+## GitLab Pages on GitLab.com
+
+If you are using GitLab.com to host your website, then:
+
+- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
+- Custom domains and TLS support are enabled.
+- Shared runners are enabled by default, provided for free and can be used to
+ build your website. If you want you can still bring your own Runner.
+
+The rest of the guide still applies.
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file for that matter, if you want to redirect a web page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## Frequently Asked Questions
+
+### Can I download my generated pages?
+
+Sure. All you need to do is download the artifacts archive from the job page.
+
+### Can I use GitLab Pages if my project is private?
+
+Yes. GitLab Pages don't care whether you set your project's visibility level
+to private, internal or public.
+
+### Do I need to create a user/group website before creating a project website?
+
+No, you don't. You can create your project first and it will be accessed under
+`http(s)://namespace.example.io/projectname`.
+
+## Known issues
+
+For a list of known issues, visit GitLab's [public issue tracker].
+
+[jekyll]: http://jekyllrb.com/
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[gitlab ci]: https://about.gitlab.com/gitlab-ci
+[gitlab runner]: https://docs.gitlab.com/runner/
+[pages]: ../../../ci/yaml/README.md#pages
+[yaml]: ../../../ci/yaml/README.md
+[staticgen]: https://www.staticgen.com/
+[pages-jekyll]: https://gitlab.com/pages/jekyll
+[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
+[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=pages
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[quick start guide]: ../../../ci/quick_start/README.md
+[pages-index-guide]: index.md
+[pages-quick]: getting_started_part_one.md
+[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index c415d566a7c..d47a3acdbe9 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -109,12 +109,19 @@ the title of the issue and as suffix it will have its ID. Thus, the example
screenshot above will yield a branch named
`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
+Since GitLab 9.0, when you click the `New branch` in an empty repository project, GitLab automatically creates the master branch, commits a blank `README.md` file to it and creates and redirects you to a new branch based on the issue title.
+If your [project is already configured with a deployment service][project-services-doc] (e.g. Kubernetes), GitLab takes one step further and prompts you to set up [auto deploy][auto-deploy-doc] by helping you create a `.gitlab-ci.yml` file.
+
+
After the branch is created, you can edit files in the repository to fix
the issue. When a merge request is created based on the newly created branch,
the description field will automatically display the [issue closing pattern]
`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
merge request is merged.
+[project-services-doc]: ../integrations/project_services.md
+[auto-deploy-doc]: ../../../ci/autodeploy/index.md
+
### Create a new branch from a project's dashboard
If you want to make changes to several files before creating a new merge
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index be042ddf623..7a4f9f408f1 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -34,7 +34,7 @@ with all their related data and be moved into a new GitLab instance.
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
| 8.9.0 | 0.1.0 |
-
+
> The table reflects what GitLab version we updated the Import/Export version at.
> For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
> and the exports between them will be compatible.
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index ad5d51d34f2..45176fde9db 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -35,3 +35,4 @@ do.
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
+| `/award :emoji:` | Toggle award for :emoji: |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9e7ee47387c..6a8de51a199 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -40,3 +40,4 @@
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
- [Snippets](../user/snippets.md)
+- [Subgroups](../user/group/subgroups/index.md)
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 1df0698afd0..d74378cc564 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,65 +1 @@
-# Award emoji
-
->**Note:**
-[Introduced][1825] in GitLab 8.2.
-
-When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. Emoji can be awarded to issues and merge requests, making
-virtual celebrations easier.
-
-![Award emoji](img/award_emoji_select.png)
-
-Award emoji make it much easier to give and receive feedback without a long
-comment thread. Comments that are only emoji will automatically become
-award emoji.
-
-## Sort issues and merge requests on vote count
-
->**Note:**
-[Introduced][2871] in GitLab 8.5.
-
-You can quickly sort issues and merge requests by the number of votes they
-have received. The sort options can be found in the dropdown menu as "Most
-popular" and "Least popular".
-
-![Votes sort options](img/award_emoji_votes_sort_options.png)
-
----
-
-Sort by most popular issues/merge requests.
-
-![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
-
----
-
-Sort by least popular issues/merge requests.
-
-![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
-
----
-
-The total number of votes is not summed up. An issue with 18 upvotes and 5
-downvotes is considered more popular than an issue with 17 upvotes and no
-downvotes.
-
-## Award emoji for comments
-
->**Note:**
-[Introduced][4291] in GitLab 8.9.
-
-Award emoji can also be applied to individual comments when you want to
-celebrate an accomplishment or agree with an opinion.
-
-To add an award emoji, click the smile in the top right of the comment and pick
-an emoji from the dropdown.
-
-![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
-
-![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
-
-If you want to remove an award emoji, just click the emoji again and the vote
-will be removed.
-
-[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
-[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
-[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+This document was moved to [another location](../user/award_emojis.md).
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 4889e3ec50c..d12c0c6d0c4 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -203,7 +203,7 @@ But the advantages of having stable identifiers outweigh this drawback.
And to understand a change in context one can always look at the merge commit that groups all the commits together when the code is merged into the master branch.
After you merge multiple commits from a feature branch into the master branch this is harder to undo.
-If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
+If you had squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
This however, requires having specific merge commits for the commits your want to revert.
If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise.
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
deleted file mode 100644
index e1b37beaf62..00000000000
--- a/doc/workflow/img/award_emoji_select.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
deleted file mode 100644
index 86ede4b0c10..00000000000
--- a/doc/workflow/img/award_emoji_votes_least_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
deleted file mode 100644
index 1d3e2e57aa0..00000000000
--- a/doc/workflow/img/award_emoji_votes_most_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
deleted file mode 100644
index c6dc1b939c1..00000000000
--- a/doc/workflow/img/award_emoji_votes_sort_options.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 8c5020bee37..6adde447975 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -4,13 +4,6 @@ Managing large files such as audio, video and graphics files has always been one
of the shortcomings of Git. The general recommendation is to not have Git repositories
larger than 1GB to preserve performance.
-GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html)
-(EE only), however in certain environments it is not always convenient to use
-different commands to differentiate between the large files and regular ones.
-
-Git LFS makes this simpler for the end user by removing the requirement to
-learn new commands.
-
## How it works
Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
@@ -63,6 +56,12 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
+>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
+ LFS will not be working properly for people cloning the project.
+ ```bash
+ git add .gitattributes
+ ```
+
Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
index dff36899aec..37afe553e55 100644
--- a/doc/workflow/milestones.md
+++ b/doc/workflow/milestones.md
@@ -1,13 +1,28 @@
# Milestones
-Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
+Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
![milestone form](milestones/form.png)
## Groups and milestones
-You can create a milestone for several projects in the same group simultaneously.
+You can create a milestone for several projects in the same group simultaneously.
On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
![group milestone form](milestones/group_form.png)
+
+## Special milestone filters
+
+In addition to the milestones that exist in the project or group, there are some
+special options available when filtering by milestone:
+
+* **No Milestone** - only show issues or merge requests without a milestone.
+* **Upcoming** - show issues or merge request that belong to the next open
+ milestone with a due date, by project. (For example: if project A has
+ milestone v1 due in three days, and project B has milestone v2 due in a week,
+ then this will show issues or merge requests from milestone v1 in project A
+ and milestone v2 in project B.)
+* **Started** - show issues or merge requests from any milestone with a start
+ date less than today. Note that this can return results from several
+ milestones in the same project.
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2a5e622dc7d..7aa9b46081a 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -47,7 +47,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>g</kbd> | Go to graphs |
+| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 92061dac7f4..b1d5e4a7acb 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,6 +11,7 @@ Feature: Dashboard
And I visit dashboard page
Scenario: I should see projects list
+ Then I should see "New Project" link
Then I should see "Shop" project link
Then I should see "Shop" project CI status
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 5c14c5db665..0d6f7350181 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -7,8 +7,9 @@ Feature: Project Active Tab
Scenario: On Project Home
Given I visit my project's home page
- Then the active main tab should be Home
- And no other main tabs should be active
+ Then the active sub tab should be Home
+ And no other sub tabs should be active
+ And the active main tab should be Project
Scenario: On Project Repository
Given I visit my project's files page
@@ -34,36 +35,45 @@ Feature: Project Active Tab
Scenario: On Project Home/Show
Given I visit my project's home page
- Then the active main tab should be Home
+ Then the active sub tab should be Home
+ And no other sub tabs should be active
+ And the active main tab should be Project
And no other main tabs should be active
+ Scenario: On Project Home/Activity
+ Given I visit my project's home page
+ And I click the "Activity" tab
+ Then the active sub tab should be Activity
+ And no other sub tabs should be active
+ And the active main tab should be Project
+
# Sub Tabs: Settings
Scenario: On Project Settings/Integrations
Given I visit my project's settings page
And I click the "Integrations" tab
- Then the active sub nav should be Integrations
- And no other sub navs should be active
+ Then the active sub tab should be Integrations
+ And no other sub tabs should be active
And the active main tab should be Settings
- Scenario: On Project Settings/Deploy Keys
+ Scenario: On Project Settings/Repository
Given I visit my project's settings page
- And I click the "Deploy Keys" tab
- Then the active sub nav should be Deploy Keys
- And no other sub navs should be active
+ And I click the "Repository" tab
+ Then the active sub tab should be Repository
+ And no other sub tabs should be active
And the active main tab should be Settings
Scenario: On Project Settings/Pages
Given I visit my project's settings page
And I click the "Pages" tab
- Then the active sub nav should be Pages
- And no other sub navs should be active
+ Then the active sub tab should be Pages
+ And no other sub tabs should be active
And the active main tab should be Settings
Scenario: On Project Members
Given I visit my project's members page
- Then the active sub nav should be Members
- And no other sub navs should be active
+ Then the active sub tab should be Members
+ And no other sub tabs should be active
And the active main tab should be Settings
# Sub Tabs: Repository
@@ -80,9 +90,9 @@ Feature: Project Active Tab
And no other sub tabs should be active
And the active main tab should be Repository
- Scenario: On Project Repository/Network
- Given I visit my project's network page
- Then the active sub tab should be Network
+ Scenario: On Project Repository/Graph
+ Given I visit my project's graph page
+ Then the active sub tab should be Graph
And no other sub tabs should be active
And the active main tab should be Repository
@@ -93,6 +103,13 @@ Feature: Project Active Tab
And no other sub tabs should be active
And the active main tab should be Repository
+ Scenario: On Project Repository/Charts
+ Given I visit my project's commits page
+ And I click the "Charts" tab
+ Then the active sub tab should be Charts
+ And no other sub tabs should be active
+ And the active main tab should be Repository
+
Scenario: On Project Repository/Branches
Given I visit my project's commits page
And I click the "Branches" tab
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 88fef674c0c..c57376aecff 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -13,6 +13,7 @@ Feature: Project Commits Branches
Given I visit project protected branches page
Then I should see "Shop" protected branches list
+ @javascript
Scenario: I create a branch
Given I visit project branches page
And I click new branch link
@@ -33,12 +34,7 @@ Feature: Project Commits Branches
And I submit new branch form with invalid name
Then I should see new an error that branch is invalid
- Scenario: I create a branch with invalid reference
- Given I visit project branches page
- And I click new branch link
- And I submit new branch form with invalid reference
- Then I should see new an error that ref is invalid
-
+ @javascript
Scenario: I create a branch that already exists
Given I visit project branches page
And I click new branch link
diff --git a/features/project/graph.feature b/features/project/graph.feature
index 63793d6f989..b25c73ad870 100644
--- a/features/project/graph.feature
+++ b/features/project/graph.feature
@@ -9,9 +9,10 @@ Feature: Project Graph
Then page should have graphs
@javascript
- Scenario: I should see project commits graphs
+ Scenario: I should see project languages & commits graphs on commits graph url
When I visit project "Shop" commits graph page
Then page should have commits graphs
+ Then page should have languages graphs
@javascript
Scenario: I should see project ci graphs
@@ -20,6 +21,13 @@ Feature: Project Graph
Then page should have CI graphs
@javascript
- Scenario: I should see project languages graphs
+ Scenario: I should see project languages & commits graphs on language graph url
When I visit project "Shop" languages graph page
Then page should have languages graphs
+ Then page should have commits graphs
+
+ @javascript
+ Scenario: I should see project languages & commits graphs on charts url
+ When I visit project "Shop" chart page
+ Then page should have languages graphs
+ Then page should have commits graphs
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
index f0fd414a9f9..1d7adfdd2c2 100644
--- a/features/project/issues/award_emoji.feature
+++ b/features/project/issues/award_emoji.feature
@@ -42,4 +42,4 @@ Feature: Award Emoji
@javascript
Scenario: I add award emoji using regular comment
Given I leave comment with a single emoji
- Then I have award added
+ Then I have new comment with emoji added
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index f71f69ef060..b47fca31ef2 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -19,15 +19,16 @@ Feature: Project Shortcuts
Then the active sub tab should be Commits
@javascript
- Scenario: Navigate to network tab
+ Scenario: Navigate to graph tab
Given I press "g" and "n"
- Then the active sub tab should be Network
+ Then the active sub tab should be Graph
And the active main tab should be Repository
@javascript
- Scenario: Navigate to graphs tab
+ Scenario: Navigate to repository charts tab
Given I press "g" and "g"
- Then the active main tab should be Graphs
+ Then the active sub tab should be Charts
+ And the active main tab should be Repository
@javascript
Scenario: Navigate to issues tab
@@ -52,9 +53,11 @@ Feature: Project Shortcuts
@javascript
Scenario: Navigate to project home
Given I press "g" and "p"
- Then the active main tab should be Home
+ Then the active sub tab should be Home
+ And the active main tab should be Project
@javascript
Scenario: Navigate to project feed
Given I press "g" and "e"
- Then the active main tab should be Activity
+ Then the active sub tab should be Activity
+ And the active main tab should be Project
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index eb906a55a83..9f01dff776f 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
def should_not_see_todo(title)
- expect(page).not_to have_content title
+ expect(page).not_to have_visible_content title
+ end
+
+ def have_visible_content(text)
+ have_css('*', text: text, visible: true)
end
def john_doe
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 2b4a5ab0864..7dc33ab5683 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
step 'I should see an http link to the repository' do
project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.http_url_to_repo)
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
end
step 'I should see an ssh link to the repository' do
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 70e23098dde..20204ad8654 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include SharedUser
step 'I click on group milestones' do
- page.within('.layout-nav') do
- click_link 'Milestones'
- end
+ visit group_milestones_path('owned')
end
step 'I should see group milestones index page has no milestones' do
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index e842d7bec2b..4befd49ac81 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -22,37 +22,53 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Edit Project"' do
- page.within '.layout-nav .controls' do
+ page.within '.sub-nav' do
click_link('Edit Project')
end
end
step 'I click the "Integrations" tab' do
- click_link('Integrations')
+ page.within '.sub-nav' do
+ click_link('Integrations')
+ end
end
- step 'I click the "Deploy Keys" tab' do
- click_link('Deploy Keys')
+ step 'I click the "Repository" tab' do
+ page.within '.sub-nav' do
+ click_link('Repository')
+ end
end
step 'I click the "Pages" tab' do
- click_link('Pages')
+ page.within '.sub-nav' do
+ click_link('Pages')
+ end
end
- step 'the active sub nav should be Members' do
- ensure_active_sub_nav('Members')
+ step 'I click the "Activity" tab' do
+ page.within '.sub-nav' do
+ click_link('Activity')
+ end
end
- step 'the active sub nav should be Integrations' do
- ensure_active_sub_nav('Integrations')
+ step 'the active sub tab should be Members' do
+ ensure_active_sub_tab('Members')
end
- step 'the active sub nav should be Deploy Keys' do
- ensure_active_sub_nav('Deploy Keys')
+ step 'the active sub tab should be Integrations' do
+ ensure_active_sub_tab('Integrations')
end
- step 'the active sub nav should be Pages' do
- ensure_active_sub_nav('Pages')
+ step 'the active sub tab should be Repository' do
+ ensure_active_sub_tab('Repository')
+ end
+
+ step 'the active sub tab should be Pages' do
+ ensure_active_sub_tab('Pages')
+ end
+
+ step 'the active sub tab should be Activity' do
+ ensure_active_sub_tab('Activity')
end
# Sub Tabs: Commits
@@ -71,6 +87,12 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
click_link('Tags')
end
+ step 'I click the "Charts" tab' do
+ page.within '.sub-nav' do
+ click_link('Charts')
+ end
+ end
+
step 'the active sub tab should be Compare' do
ensure_active_sub_tab('Compare')
end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 055fca036d3..be0f6eee55a 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -76,7 +76,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
params = JSON.parse(Base64.urlsafe_decode64(base64_params))
- expect(params.keys).to eq(['Archive', 'Entry'])
+ expect(params.keys).to eq(%w(Archive Entry))
expect(params['Archive']).to end_with('build_artifacts.zip')
expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 5f9b9e0445e..ccaf3237815 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form' do
fill_in 'branch_name', with: 'deploy_keys'
- fill_in 'ref', with: 'master'
+ select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable'
- fill_in 'ref', with: 'master'
- click_button 'Create branch'
- end
-
- step 'I submit new branch form with invalid reference' do
- fill_in 'branch_name', with: 'foo'
- fill_in 'ref', with: 'foo'
+ select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with branch that already exists' do
fill_in 'branch_name', with: 'master'
- fill_in 'ref', with: 'master'
+ select_branch('master')
click_button 'Create branch'
end
@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content "can't contain spaces"
end
- step 'I should see new an error that ref is invalid' do
- expect(page).to have_content 'Invalid reference name'
- end
-
step 'I should see new an error that branch already exists' do
expect(page).to have_content 'Branch already exists'
end
@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I should not see branch 'improve/awesome'" do
expect(page.all(visible: true)).not_to have_content 'improve/awesome'
end
+
+ def select_branch(branch_name)
+ click_button 'master'
+
+ page.within '#new-branch-form .dropdown-menu' do
+ click_link branch_name
+ end
+ end
end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 18e267294e4..cf75fac8ac6 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
- expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
+ expect(page).to have_content "Pipeline #1 pending"
end
step 'I search "submodules" commits' do
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
index 94a5d4e2e4d..c9746407344 100644
--- a/features/steps/project/commits/revert.rb
+++ b/features/steps/project/commits/revert.rb
@@ -36,5 +36,6 @@ class Spinach::Features::RevertCommits < Spinach::FeatureSteps
step 'I should see the new merge request notice' do
page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
end
end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index edf78f62f9a..580a19494c2 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I should be on deploy keys page' do
- expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project)
+ expect(current_path).to eq namespace_project_settings_repository_path(@project.namespace, @project)
end
step 'I should see newly created deploy key' do
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 9a6c04fba7a..79db9728227 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -56,7 +56,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I should see my fork on the list' do
- page.within('.projects-list-holder') do
+ page.within('.js-projects-list-holder') do
project = @user.fork_of(@project)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 48ac7a98f0d..176d04d721c 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -18,6 +18,10 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
visit languages_namespace_project_graph_path(project.namespace, project, "master")
end
+ step 'I visit project "Shop" chart page' do
+ visit charts_namespace_project_graph_path(project.namespace, project, "master")
+ end
+
step 'page should have languages graphs' do
expect(page).to have_content /Ruby 66.* %/
expect(page).to have_content /JavaScript 22.* %/
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index cbe5738e7e4..1762d5bdf95 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -44,6 +44,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
end
+ step 'I have new comment with emoji added' do
+ expect(page).to have_selector ".emoji[title=':smile:']"
+ end
+
step 'I have award added' do
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
@@ -86,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I see search result for "hand"' do
page.within '.emoji-menu-content' do
- expect(page).to have_selector '[data-emoji="raised_hand"]'
+ expect(page).to have_selector '[data-name="raised_hand"]'
end
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index ff9251615c9..370e46265c7 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -66,7 +66,7 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should have "v1.0.0" in title' do
- expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false
+ expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false
end
step 'page should only have content from "v1.0.0"' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
index c80c6273807..4045955a8b9 100644
--- a/features/steps/project/pages.rb
+++ b/features/steps/project/pages.rb
@@ -53,13 +53,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
end
step 'pages are exposed on external HTTP address' do
- allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
end
step 'pages are exposed on external HTTPS address' do
- allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
- allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
end
step 'I should be able to add a New Domain' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index f18adcadcce..5c47eaf0279 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -48,7 +48,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click link "Raw"' do
- click_link 'Raw'
+ click_link 'Open raw'
end
step 'I should see raw file content' do
@@ -82,7 +82,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
- fill_in :target_branch, with: 'new_branch_name', visible: true
+ first('button.js-target-branch', visible: true).click
+ first('.create-new-branch', visible: true).click
+ first('#new_branch_name', visible: true).set('new_branch_name')
+ first('.js-new-branch-btn', visible: true).click
end
step 'I fill the new file name with an illegal name' do
@@ -334,6 +337,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click on "files/lfs/lfs_object.iso" file in repo' do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
visit namespace_project_tree_path(@project.namespace, @project, "lfs")
click_link 'files'
click_link "lfs"
@@ -352,7 +356,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see buttons for allowed commands' do
page.within '.content' do
- expect(page).to have_content 'Raw'
+ expect(page).to have_link 'Open raw'
expect(page).to have_content 'History'
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 79dde620265..3d9cedf5c2d 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -153,7 +153,7 @@ module SharedIssuable
case type
when :issue
- attrs.merge!(project: project)
+ attrs[:project] = project
when :merge_request
attrs.merge!(
source_project: project,
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 718cf924729..d5b3bb34d7a 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -232,7 +232,7 @@ module SharedPaths
visit stats_namespace_project_repository_path(@project.namespace, @project)
end
- step "I visit my project's network page" do
+ step "I visit my project's graph page" do
# Stub Graph max_size to speed up test (10 commits vs. 650)
Network::Graph.stub(max_count: 10)
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index dae248b8b7e..345a28f27dc 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -166,11 +166,15 @@ module SharedProject
end
step 'I should see project "Internal"' do
- expect(page).to have_content "Internal"
+ page.within '.js-projects-list-holder' do
+ expect(page).to have_content "Internal"
+ end
end
step 'I should not see project "Internal"' do
- expect(page).not_to have_content "Internal"
+ page.within '.js-projects-list-holder' do
+ expect(page).not_to have_content "Internal"
+ end
end
step 'public project "Community"' do
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index d6024212601..0cb9229dbae 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -4,7 +4,7 @@ module SharedProjectTab
include Spinach::DSL
include SharedActiveTab
- step 'the active main tab should be Home' do
+ step 'the active main tab should be Project' do
ensure_active_main_tab('Project')
end
@@ -12,16 +12,12 @@ module SharedProjectTab
ensure_active_main_tab('Repository')
end
- step 'the active main tab should be Graphs' do
- ensure_active_main_tab('Graphs')
- end
-
step 'the active main tab should be Issues' do
ensure_active_main_tab('Issues')
end
- step 'the active main tab should be Members' do
- ensure_active_main_tab('Members')
+ step 'the active sub tab should be Members' do
+ ensure_active_sub_tab('Members')
end
step 'the active main tab should be Merge Requests' do
@@ -37,15 +33,11 @@ module SharedProjectTab
end
step 'the active main tab should be Settings' do
- expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0)
+ ensure_active_main_tab('Settings')
end
- step 'the active main tab should be Activity' do
- ensure_active_main_tab('Activity')
- end
-
- step 'the active sub tab should be Network' do
- ensure_active_sub_tab('Network')
+ step 'the active sub tab should be Graph' do
+ ensure_active_sub_tab('Graph')
end
step 'the active sub tab should be Files' do
@@ -55,4 +47,16 @@ module SharedProjectTab
step 'the active sub tab should be Commits' do
ensure_active_sub_tab('Commits')
end
+
+ step 'the active sub tab should be Home' do
+ ensure_active_sub_tab('Home')
+ end
+
+ step 'the active sub tab should be Activity' do
+ ensure_active_sub_tab('Activity')
+ end
+
+ step 'the active sub tab should be Charts' do
+ ensure_active_sub_tab('Charts')
+ end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 47372df152d..c0c489d2775 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,8 +1,9 @@
require 'spinach/capybara'
require 'capybara/poltergeist'
+require 'capybara-screenshot/spinach'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
@@ -20,12 +21,8 @@ end
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = false
-unless ENV['CI'] || ENV['CI_SERVER']
- require 'capybara-screenshot/spinach'
-
- # Keep only the screenshots generated from the last failing test suite
- Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 078d3413f33..3cbc4702dac 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -1,11622 +1,10748 @@
-[
- {
- "name": "100",
- "unicode": "1F4AF",
+{
+ "100": {
+ "category": "symbols",
+ "moji": "💯",
+ "unicodeVersion": "6.0",
"digest": "add3bd7d06b6dd445788b277f8c9e5dcf42a54d3ec8b7fb9e7a39695dd95d094"
},
- {
- "name": "1234",
- "unicode": "1F522",
+ "1234": {
+ "category": "symbols",
+ "moji": "🔢",
+ "unicodeVersion": "6.0",
"digest": "c5ac5c8147f5bfd644fad6b470432bba86ffc7bcee04a0e0d277cd1ca485207f"
},
- {
- "name": "8ball",
- "unicode": "1F3B1",
+ "8ball": {
+ "category": "activity",
+ "moji": "🎱",
+ "unicodeVersion": "6.0",
"digest": "a6e6855775b66c505adee65926a264103ebddf2e2d963db7c009b4fec3a24178"
},
- {
- "name": "a",
- "unicode": "1F170",
+ "a": {
+ "category": "symbols",
+ "moji": "🅰",
+ "unicodeVersion": "6.0",
"digest": "bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc"
},
- {
- "name": "ab",
- "unicode": "1F18E",
+ "ab": {
+ "category": "symbols",
+ "moji": "🆎",
+ "unicodeVersion": "6.0",
"digest": "67430fe5fce981160e2ea9052962e49f264322d3abfc2828fbc311b6cdf67ae8"
},
- {
- "name": "abc",
- "unicode": "1F524",
+ "abc": {
+ "category": "symbols",
+ "moji": "🔤",
+ "unicodeVersion": "6.0",
"digest": "282c817ee3414d77a74b815962c33dd9fe71fabaea8c7a9cec466100fbe32187"
},
- {
- "name": "abcd",
- "unicode": "1F521",
+ "abcd": {
+ "category": "symbols",
+ "moji": "🔡",
+ "unicodeVersion": "6.0",
"digest": "686728c759f4683c64762ee4eda0a91bf2041f0ae4f358aacf6c09bf51892eff"
},
- {
- "name": "accept",
- "unicode": "1F251",
+ "accept": {
+ "category": "symbols",
+ "moji": "🉑",
+ "unicodeVersion": "6.0",
"digest": "7208d34c761f10a7fd28f98e25535eba13ff91a64442fc282a98bb77722614f1"
},
- {
- "name": "aerial_tramway",
- "unicode": "1F6A1",
+ "aerial_tramway": {
+ "category": "travel",
+ "moji": "🚡",
+ "unicodeVersion": "6.0",
"digest": "98df666f34370fc34ce280d84bba5a7e617f733fbbfe66caa424b2afa6ab6777"
},
- {
- "name": "airplane",
- "unicode": "2708",
+ "airplane": {
+ "category": "travel",
+ "moji": "✈",
+ "unicodeVersion": "1.1",
"digest": "cc12cf259ef88e57717620cd2bd5aa6a02a8631ee532a3bde24bee78edc5de33"
},
- {
- "name": "airplane_arriving",
- "unicode": "1F6EC",
+ "airplane_arriving": {
+ "category": "travel",
+ "moji": "🛬",
+ "unicodeVersion": "7.0",
"digest": "80d5b4675f91c4cff06d146d795a065b0ce2a74557df4d9e3314e3d3b5c4ae82"
},
- {
- "name": "airplane_departure",
- "unicode": "1F6EB",
+ "airplane_departure": {
+ "category": "travel",
+ "moji": "🛫",
+ "unicodeVersion": "7.0",
"digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332"
},
- {
- "name": "airplane_small",
- "unicode": "1F6E9",
+ "airplane_small": {
+ "category": "travel",
+ "moji": "🛩",
+ "unicodeVersion": "7.0",
"digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
},
- {
- "name": "small_airplane",
- "unicode": "1F6E9",
- "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
- },
- {
- "name": "alarm_clock",
- "unicode": "23F0",
+ "alarm_clock": {
+ "category": "objects",
+ "moji": "⏰",
+ "unicodeVersion": "6.0",
"digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599"
},
- {
- "name": "alembic",
- "unicode": "2697",
+ "alembic": {
+ "category": "objects",
+ "moji": "⚗",
+ "unicodeVersion": "4.1",
"digest": "c94b2a4bf24ccf4db27a22c9725cfe900f4a99ec49ef2411d67952bcb2ca1bfb"
},
- {
- "name": "alien",
- "unicode": "1F47D",
+ "alien": {
+ "category": "people",
+ "moji": "👽",
+ "unicodeVersion": "6.0",
"digest": "856ba98202b244c13a5ee3014a6f7ad592d8c119a30d79e4fc790b74b0e321f7"
},
- {
- "name": "ambulance",
- "unicode": "1F691",
+ "ambulance": {
+ "category": "travel",
+ "moji": "🚑",
+ "unicodeVersion": "6.0",
"digest": "d9b3c1873de496a4554e715342c72290fb69a9c6766d7885f38bfe9491d052da"
},
- {
- "name": "amphora",
- "unicode": "1F3FA",
+ "amphora": {
+ "category": "objects",
+ "moji": "🏺",
+ "unicodeVersion": "8.0",
"digest": "4015f907b649b5e348502cc0e3685ed184e180dca5cc81c43ec516e14df127bf"
},
- {
- "name": "anchor",
- "unicode": "2693",
+ "anchor": {
+ "category": "travel",
+ "moji": "⚓",
+ "unicodeVersion": "4.1",
"digest": "2b29b34ef896ebab70016301e3d1880209bbc3c5a5b8d832e43afff9b17ad792"
},
- {
- "name": "angel",
- "unicode": "1F47C",
+ "angel": {
+ "category": "people",
+ "moji": "👼",
+ "unicodeVersion": "6.0",
"digest": "db75c2460aaf9cd07cb41fe22c8a6079f3667ffe612a71611358720e2b5512a4"
},
- {
- "name": "angel_tone1",
- "unicode": "1F47C-1F3FB",
+ "angel_tone1": {
+ "category": "people",
+ "moji": "👼🏻",
+ "unicodeVersion": "8.0",
"digest": "5871a622469b96296365adaf77d83167759692124c20e5a6e062a525af33472a"
},
- {
- "name": "angel_tone2",
- "unicode": "1F47C-1F3FC",
+ "angel_tone2": {
+ "category": "people",
+ "moji": "👼🏼",
+ "unicodeVersion": "8.0",
"digest": "f5993198a5d9daf39e761c783461f07bca237f4e9b739ac300bb8ca001a69a1a"
},
- {
- "name": "angel_tone3",
- "unicode": "1F47C-1F3FD",
+ "angel_tone3": {
+ "category": "people",
+ "moji": "👼🏽",
+ "unicodeVersion": "8.0",
"digest": "f0c97a7c4354626267d6ab0f388e4297ad255ab9b061f9c68fbcaa0abfc52783"
},
- {
- "name": "angel_tone4",
- "unicode": "1F47C-1F3FE",
+ "angel_tone4": {
+ "category": "people",
+ "moji": "👼🏾",
+ "unicodeVersion": "8.0",
"digest": "6e5dc724c1939d1b0d1a91343662b5bd61ced7709c97802977145ffab6a1f7ac"
},
- {
- "name": "angel_tone5",
- "unicode": "1F47C-1F3FF",
+ "angel_tone5": {
+ "category": "people",
+ "moji": "👼🏿",
+ "unicodeVersion": "8.0",
"digest": "52186e1de350c27d25d6010edf44f64a30338b65912ca178429fbcfbd88113c2"
},
- {
- "name": "anger",
- "unicode": "1F4A2",
+ "anger": {
+ "category": "symbols",
+ "moji": "💢",
+ "unicodeVersion": "6.0",
"digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f"
},
- {
- "name": "anger_right",
- "unicode": "1F5EF",
+ "anger_right": {
+ "category": "symbols",
+ "moji": "🗯",
+ "unicodeVersion": "7.0",
"digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
},
- {
- "name": "right_anger_bubble",
- "unicode": "1F5EF",
- "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
- },
- {
- "name": "angry",
- "unicode": "1F620",
+ "angry": {
+ "category": "people",
+ "moji": "😠",
+ "unicodeVersion": "6.0",
"digest": "7e09e7e821f511606341fb5ce4011a8ed9809766ab86b7983ffa6ea352b39ec1"
},
- {
- "name": "anguished",
- "unicode": "1F627",
- "digest": "a2b6f052996969a17150249d9ef5db742da3d6585bd38ca61eb14c4c13cda54f"
- },
- {
- "name": "ant",
- "unicode": "1F41C",
+ "ant": {
+ "category": "nature",
+ "moji": "🐜",
+ "unicodeVersion": "6.0",
"digest": "929abeaff7ba21ab71cd1ab798af7a6b611e3b3ce1af80cede09a116b223e442"
},
- {
- "name": "apple",
- "unicode": "1F34E",
+ "apple": {
+ "category": "food",
+ "moji": "🍎",
+ "unicodeVersion": "6.0",
"digest": "2a1b85ce57e3d236ae7777dcf332ec37d03bfd7b19806521a353bc532083224d"
},
- {
- "name": "aquarius",
- "unicode": "2652",
+ "aquarius": {
+ "category": "symbols",
+ "moji": "♒",
+ "unicodeVersion": "1.1",
"digest": "fdc42cd41b0dace5eae6baba3143f1e40295d48a29e7103a5bba1d84a056c39d"
},
- {
- "name": "aries",
- "unicode": "2648",
+ "aries": {
+ "category": "symbols",
+ "moji": "♈",
+ "unicodeVersion": "1.1",
"digest": "deb135debcde0a98f40361a84ab64d57c18b5b445cd2f4199e8936f052899737"
},
- {
- "name": "arrow_backward",
- "unicode": "25C0",
+ "arrow_backward": {
+ "category": "symbols",
+ "moji": "◀",
+ "unicodeVersion": "1.1",
"digest": "e162ac82e90d1e925d479fa5c45b9340e0a53287be04e43cbbb2a89c7e7e45e4"
},
- {
- "name": "arrow_double_down",
- "unicode": "23EC",
+ "arrow_double_down": {
+ "category": "symbols",
+ "moji": "⏬",
+ "unicodeVersion": "6.0",
"digest": "03ca890b05338d40972c7a056d672df620a203c6ca52ff3ff530f1a710905507"
},
- {
- "name": "arrow_double_up",
- "unicode": "23EB",
+ "arrow_double_up": {
+ "category": "symbols",
+ "moji": "⏫",
+ "unicodeVersion": "6.0",
"digest": "e753f05bce993d62d5dc79e33c441ced059381b6ce21fa3ea4200f1b3236e59d"
},
- {
- "name": "arrow_down",
- "unicode": "2B07",
+ "arrow_down": {
+ "category": "symbols",
+ "moji": "⬇",
+ "unicodeVersion": "4.0",
"digest": "9bf1bd2ea652ca9321087de58c7a112ea04c35676a6ee0766154183f8b95af6c"
},
- {
- "name": "arrow_down_small",
- "unicode": "1F53D",
+ "arrow_down_small": {
+ "category": "symbols",
+ "moji": "🔽",
+ "unicodeVersion": "6.0",
"digest": "7766198bc60cf59d6cdaeeaa700c2282bfff2f0fdeb22cf4581ca284b87a3bb7"
},
- {
- "name": "arrow_forward",
- "unicode": "25B6",
+ "arrow_forward": {
+ "category": "symbols",
+ "moji": "▶",
+ "unicodeVersion": "1.1",
"digest": "db77d9accd1e02224f5d612f79cd691e6befdf22063475204836be6572510fb7"
},
- {
- "name": "arrow_heading_down",
- "unicode": "2935",
+ "arrow_heading_down": {
+ "category": "symbols",
+ "moji": "⤵",
+ "unicodeVersion": "3.2",
"digest": "f5396069c8f63c13e6c3e0ecd34267c932451309ade9c1171d410563153bf909"
},
- {
- "name": "arrow_heading_up",
- "unicode": "2934",
+ "arrow_heading_up": {
+ "category": "symbols",
+ "moji": "⤴",
+ "unicodeVersion": "3.2",
"digest": "1cad71923fa3df24cf543cae4ce775b0f74936f2edd685fd86a7525c41a14568"
},
- {
- "name": "arrow_left",
- "unicode": "2B05",
+ "arrow_left": {
+ "category": "symbols",
+ "moji": "⬅",
+ "unicodeVersion": "4.0",
"digest": "b629bb3dbe161ef89cfcfced0c7968a68e44a019ad509132987e4973bdc874e7"
},
- {
- "name": "arrow_lower_left",
- "unicode": "2199",
+ "arrow_lower_left": {
+ "category": "symbols",
+ "moji": "↙",
+ "unicodeVersion": "1.1",
"digest": "879136ba0e24e6bf3be70118abcb716d71bd74f7b62347bc052b6533c0ea534d"
},
- {
- "name": "arrow_lower_right",
- "unicode": "2198",
+ "arrow_lower_right": {
+ "category": "symbols",
+ "moji": "↘",
+ "unicodeVersion": "1.1",
"digest": "86d52ac9b961991e3aaa6a9f9b5ace4db6ffd1b5c171c09c23b516473b55066d"
},
- {
- "name": "arrow_right",
- "unicode": "27A1",
+ "arrow_right": {
+ "category": "symbols",
+ "moji": "➡",
+ "unicodeVersion": "1.1",
"digest": "45f26a1cbb0f00ed3609b39da52e9d9e896a77e361c4c8036b1bf8038171bd49"
},
- {
- "name": "arrow_right_hook",
- "unicode": "21AA",
+ "arrow_right_hook": {
+ "category": "symbols",
+ "moji": "↪",
+ "unicodeVersion": "1.1",
"digest": "4f452679c71bcea4fc4a701c55156fef3ddc1ebbc70570bedfc9d3a029637ab1"
},
- {
- "name": "arrow_up",
- "unicode": "2B06",
+ "arrow_up": {
+ "category": "symbols",
+ "moji": "⬆",
+ "unicodeVersion": "4.0",
"digest": "982b988ef6651d8a71867ba7c87f640f62dd0eeb0b7c358f5a5c37e8fe507b8b"
},
- {
- "name": "arrow_up_down",
- "unicode": "2195",
+ "arrow_up_down": {
+ "category": "symbols",
+ "moji": "↕",
+ "unicodeVersion": "1.1",
"digest": "645ed8fb6646f49bfd95af1752336deacdadbe5cba13904023a704288f3b0e2c"
},
- {
- "name": "arrow_up_small",
- "unicode": "1F53C",
+ "arrow_up_small": {
+ "category": "symbols",
+ "moji": "🔼",
+ "unicodeVersion": "6.0",
"digest": "4a8c5789c13a852517e639e7a62c2d331464e6fb0358985aa97c1515e97b5e8b"
},
- {
- "name": "arrow_upper_left",
- "unicode": "2196",
+ "arrow_upper_left": {
+ "category": "symbols",
+ "moji": "↖",
+ "unicodeVersion": "1.1",
"digest": "79026f828d6ceb7c55a9542770962ba6dcd08203995f6ceeb70333a12307d376"
},
- {
- "name": "arrow_upper_right",
- "unicode": "2197",
+ "arrow_upper_right": {
+ "category": "symbols",
+ "moji": "↗",
+ "unicodeVersion": "1.1",
"digest": "7e0f33dfbe65628991c170130d366a3e2cedaf8862ddfcaf3960f395d3da1926"
},
- {
- "name": "arrows_clockwise",
- "unicode": "1F503",
+ "arrows_clockwise": {
+ "category": "symbols",
+ "moji": "🔃",
+ "unicodeVersion": "6.0",
"digest": "88669679977f7157f0acaa9d6a1b77ccf84d25eb78c5bc8afcde38d3635e7144"
},
- {
- "name": "arrows_counterclockwise",
- "unicode": "1F504",
+ "arrows_counterclockwise": {
+ "category": "symbols",
+ "moji": "🔄",
+ "unicodeVersion": "6.0",
"digest": "a2c6a6d3643c128aee3304cd03bb3d7cfe4d35d3ba825bc9c1142d7832b4426e"
},
- {
- "name": "art",
- "unicode": "1F3A8",
+ "art": {
+ "category": "activity",
+ "moji": "🎨",
+ "unicodeVersion": "6.0",
"digest": "b6bc6c4bfb594aadcbb641d006031867678504764bbe0ab84e7b08567a9498da"
},
- {
- "name": "articulated_lorry",
- "unicode": "1F69B",
+ "articulated_lorry": {
+ "category": "travel",
+ "moji": "🚛",
+ "unicodeVersion": "6.0",
"digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa"
},
- {
- "name": "asterisk",
- "unicode": "002A-20E3",
+ "asterisk": {
+ "category": "symbols",
+ "moji": "*⃣",
+ "unicodeVersion": "3.0",
"digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
},
- {
- "name": "keycap_asterisk",
- "unicode": "002A-20E3",
- "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
- },
- {
- "name": "astonished",
- "unicode": "1F632",
+ "astonished": {
+ "category": "people",
+ "moji": "😲",
+ "unicodeVersion": "6.0",
"digest": "f8531bdda5070d10492709085f4ff652b8be9be6458758940358b9fc594a1f14"
},
- {
- "name": "athletic_shoe",
- "unicode": "1F45F",
+ "athletic_shoe": {
+ "category": "people",
+ "moji": "👟",
+ "unicodeVersion": "6.0",
"digest": "1f90dc390e0dea679085465b7f9e786dfd7dd56a3b219987144ed37ab1e9bf95"
},
- {
- "name": "atm",
- "unicode": "1F3E7",
+ "atm": {
+ "category": "symbols",
+ "moji": "🏧",
+ "unicodeVersion": "6.0",
"digest": "7d3ce6a6afb4951546883404b8e36904179f88f1aa533706cf7bf0bbe0d6fd3c"
},
- {
- "name": "atom",
- "unicode": "269B",
- "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
- },
- {
- "name": "atom_symbol",
- "unicode": "269B",
+ "atom": {
+ "category": "symbols",
+ "moji": "⚛",
+ "unicodeVersion": "4.1",
"digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
},
- {
- "name": "avocado",
- "unicode": "1F951",
+ "avocado": {
+ "category": "food",
+ "moji": "🥑",
+ "unicodeVersion": "9.0",
"digest": "bc1fb203d63b18985598400925de24050bb192afda1cbf0813f85cb139869eff"
},
- {
- "name": "b",
- "unicode": "1F171",
+ "b": {
+ "category": "symbols",
+ "moji": "🅱",
+ "unicodeVersion": "6.0",
"digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf"
},
- {
- "name": "baby",
- "unicode": "1F476",
+ "baby": {
+ "category": "people",
+ "moji": "👶",
+ "unicodeVersion": "6.0",
"digest": "219ae5a571aaf90c060956cd1c56dcc27708c827cecdca3ba1122058a3c4847b"
},
- {
- "name": "baby_bottle",
- "unicode": "1F37C",
+ "baby_bottle": {
+ "category": "food",
+ "moji": "🍼",
+ "unicodeVersion": "6.0",
"digest": "4fb71689e9d634e8d1699cf454a71e43f2b5b1a5dbab0bf186626934fdf5b782"
},
- {
- "name": "baby_chick",
- "unicode": "1F424",
+ "baby_chick": {
+ "category": "nature",
+ "moji": "🐤",
+ "unicodeVersion": "6.0",
"digest": "14119874e9b5548028dfb9cc593a541efc1d075ac839a565b92e0c3253cffe7e"
},
- {
- "name": "baby_symbol",
- "unicode": "1F6BC",
+ "baby_symbol": {
+ "category": "symbols",
+ "moji": "🚼",
+ "unicodeVersion": "6.0",
"digest": "fb4db66868cda45ea3879ffc2ff4f763c56d2d889ae0ab17fe171129ede02f98"
},
- {
- "name": "baby_tone1",
- "unicode": "1F476-1F3FB",
+ "baby_tone1": {
+ "category": "people",
+ "moji": "👶🏻",
+ "unicodeVersion": "8.0",
"digest": "cd3faf223a298c34e05d469d9d0db08438d97df7fd82c0973f8a9e07d553f5b1"
},
- {
- "name": "baby_tone2",
- "unicode": "1F476-1F3FC",
+ "baby_tone2": {
+ "category": "people",
+ "moji": "👶🏼",
+ "unicodeVersion": "8.0",
"digest": "5b4539e22e0dd726c27eb8af2357f9240a52aed3f710f3234571cff029cc6198"
},
- {
- "name": "baby_tone3",
- "unicode": "1F476-1F3FD",
+ "baby_tone3": {
+ "category": "people",
+ "moji": "👶🏽",
+ "unicodeVersion": "8.0",
"digest": "720e740e1ac63c6372269132b1fb6e07a6b91f5c808cc3adef59f0b4500e5e72"
},
- {
- "name": "baby_tone4",
- "unicode": "1F476-1F3FE",
+ "baby_tone4": {
+ "category": "people",
+ "moji": "👶🏾",
+ "unicodeVersion": "8.0",
"digest": "5e43b69c509bd526ad6f081764578c30b6f3285fb7442222e05ccf62e53bfb64"
},
- {
- "name": "baby_tone5",
- "unicode": "1F476-1F3FF",
+ "baby_tone5": {
+ "category": "people",
+ "moji": "👶🏿",
+ "unicodeVersion": "8.0",
"digest": "85bba6e0940ccfb99999fe124e815f9dd340d00a5568e13967b02245a62dbf54"
},
- {
- "name": "back",
- "unicode": "1F519",
+ "back": {
+ "category": "symbols",
+ "moji": "🔙",
+ "unicodeVersion": "6.0",
"digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e"
},
- {
- "name": "bacon",
- "unicode": "1F953",
+ "bacon": {
+ "category": "food",
+ "moji": "🥓",
+ "unicodeVersion": "9.0",
"digest": "18ad3817f1f88a69706db5727a58e763dde6c21a2d4f184c3d728c32dc5fa05a"
},
- {
- "name": "badminton",
- "unicode": "1F3F8",
+ "badminton": {
+ "category": "activity",
+ "moji": "🏸",
+ "unicodeVersion": "8.0",
"digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66"
},
- {
- "name": "baggage_claim",
- "unicode": "1F6C4",
+ "baggage_claim": {
+ "category": "symbols",
+ "moji": "🛄",
+ "unicodeVersion": "6.0",
"digest": "7d6bceca92c266da6d2b91dfcf244546fc11022e039e7da8e6888c1696bb2186"
},
- {
- "name": "balloon",
- "unicode": "1F388",
+ "balloon": {
+ "category": "objects",
+ "moji": "🎈",
+ "unicodeVersion": "6.0",
"digest": "65760aedc1503b426927cff78c24449d563843a274961d962718fa9638375d54"
},
- {
- "name": "ballot_box",
- "unicode": "1F5F3",
+ "ballot_box": {
+ "category": "objects",
+ "moji": "🗳",
+ "unicodeVersion": "7.0",
"digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
},
- {
- "name": "ballot_box_with_ballot",
- "unicode": "1F5F3",
- "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
- },
- {
- "name": "ballot_box_with_check",
- "unicode": "2611",
+ "ballot_box_with_check": {
+ "category": "symbols",
+ "moji": "☑",
+ "unicodeVersion": "1.1",
"digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134"
},
- {
- "name": "bamboo",
- "unicode": "1F38D",
+ "bamboo": {
+ "category": "nature",
+ "moji": "🎍",
+ "unicodeVersion": "6.0",
"digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd"
},
- {
- "name": "banana",
- "unicode": "1F34C",
+ "banana": {
+ "category": "food",
+ "moji": "🍌",
+ "unicodeVersion": "6.0",
"digest": "f9e8ff910c282c20a8907ff64926b5de4ee250529a1ed718fb33302e6fff8dd9"
},
- {
- "name": "bangbang",
- "unicode": "203C",
+ "bangbang": {
+ "category": "symbols",
+ "moji": "‼",
+ "unicodeVersion": "1.1",
"digest": "76536fee63fe964a3f3839d309b1f45028fb0c43f4d1eeee495f17e1532b4def"
},
- {
- "name": "bank",
- "unicode": "1F3E6",
+ "bank": {
+ "category": "travel",
+ "moji": "🏦",
+ "unicodeVersion": "6.0",
"digest": "f5d2976bf6d521638ccacc74be06bd4abfeab06c5d898a9d245edad45a5b6306"
},
- {
- "name": "bar_chart",
- "unicode": "1F4CA",
+ "bar_chart": {
+ "category": "objects",
+ "moji": "📊",
+ "unicodeVersion": "6.0",
"digest": "65a328a1b2d7a5332dd4d93f4dbca13d976f0a505b00835c3fc458e394804240"
},
- {
- "name": "barber",
- "unicode": "1F488",
+ "barber": {
+ "category": "objects",
+ "moji": "💈",
+ "unicodeVersion": "6.0",
"digest": "5e8053d3bb3765a8632fd1cbfe21163f74ed79f6be377eb9603eaaf883d8dc46"
},
- {
- "name": "baseball",
- "unicode": "26BE",
+ "baseball": {
+ "category": "activity",
+ "moji": "⚾",
+ "unicodeVersion": "5.2",
"digest": "46ac16f8b5455b942f6dbff9483a6fd277721e6719d2731573baabd21c44b34f"
},
- {
- "name": "basketball",
- "unicode": "1F3C0",
+ "basketball": {
+ "category": "activity",
+ "moji": "🏀",
+ "unicodeVersion": "6.0",
"digest": "cc83e2aea8fcd2e9a5789e1932ee3766c40843c142fd3565c4e77dafb21ec7d7"
},
- {
- "name": "basketball_player",
- "unicode": "26F9",
- "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
- },
- {
- "name": "person_with_ball",
- "unicode": "26F9",
+ "basketball_player": {
+ "category": "activity",
+ "moji": "⛹",
+ "unicodeVersion": "5.2",
"digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9"
},
- {
- "name": "basketball_player_tone1",
- "unicode": "26F9-1F3FB",
+ "basketball_player_tone1": {
+ "category": "activity",
+ "moji": "⛹🏻",
+ "unicodeVersion": "8.0",
"digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
},
- {
- "name": "person_with_ball_tone1",
- "unicode": "26F9-1F3FB",
- "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f"
- },
- {
- "name": "basketball_player_tone2",
- "unicode": "26F9-1F3FC",
- "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
- },
- {
- "name": "person_with_ball_tone2",
- "unicode": "26F9-1F3FC",
+ "basketball_player_tone2": {
+ "category": "activity",
+ "moji": "⛹🏼",
+ "unicodeVersion": "8.0",
"digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3"
},
- {
- "name": "basketball_player_tone3",
- "unicode": "26F9-1F3FD",
+ "basketball_player_tone3": {
+ "category": "activity",
+ "moji": "⛹🏽",
+ "unicodeVersion": "8.0",
"digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
},
- {
- "name": "person_with_ball_tone3",
- "unicode": "26F9-1F3FD",
- "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac"
- },
- {
- "name": "basketball_player_tone4",
- "unicode": "26F9-1F3FE",
+ "basketball_player_tone4": {
+ "category": "activity",
+ "moji": "⛹🏾",
+ "unicodeVersion": "8.0",
"digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
},
- {
- "name": "person_with_ball_tone4",
- "unicode": "26F9-1F3FE",
- "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720"
- },
- {
- "name": "basketball_player_tone5",
- "unicode": "26F9-1F3FF",
+ "basketball_player_tone5": {
+ "category": "activity",
+ "moji": "⛹🏿",
+ "unicodeVersion": "8.0",
"digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
},
- {
- "name": "person_with_ball_tone5",
- "unicode": "26F9-1F3FF",
- "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
- },
- {
- "name": "bat",
- "unicode": "1F987",
+ "bat": {
+ "category": "nature",
+ "moji": "🦇",
+ "unicodeVersion": "9.0",
"digest": "8fc19e0d7d6f80906bdbc06d616a810de66180d96cf28070a53fa61b88904535"
},
- {
- "name": "bath",
- "unicode": "1F6C0",
+ "bath": {
+ "category": "activity",
+ "moji": "🛀",
+ "unicodeVersion": "6.0",
"digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917"
},
- {
- "name": "bath_tone1",
- "unicode": "1F6C0-1F3FB",
+ "bath_tone1": {
+ "category": "activity",
+ "moji": "🛀🏻",
+ "unicodeVersion": "8.0",
"digest": "7ae2989e47788ba71359d52da68feec95aaff68a77d5a6556957df1617af8536"
},
- {
- "name": "bath_tone2",
- "unicode": "1F6C0-1F3FC",
+ "bath_tone2": {
+ "category": "activity",
+ "moji": "🛀🏼",
+ "unicodeVersion": "8.0",
"digest": "2e86f8edad54d15a7094cd52160cbe51d10aa1750cfb0b3b58e93533f070e327"
},
- {
- "name": "bath_tone3",
- "unicode": "1F6C0-1F3FD",
+ "bath_tone3": {
+ "category": "activity",
+ "moji": "🛀🏽",
+ "unicodeVersion": "8.0",
"digest": "654c0cd083a67ff330a38d07352876d265390e5399e5352598d64a6c7e5eeba7"
},
- {
- "name": "bath_tone4",
- "unicode": "1F6C0-1F3FE",
+ "bath_tone4": {
+ "category": "activity",
+ "moji": "🛀🏾",
+ "unicodeVersion": "8.0",
"digest": "adad88c6830f31c4b5be194d1987d6aadf4adf45e4cb7f2e4657f0d20c0d663a"
},
- {
- "name": "bath_tone5",
- "unicode": "1F6C0-1F3FF",
+ "bath_tone5": {
+ "category": "activity",
+ "moji": "🛀🏿",
+ "unicodeVersion": "8.0",
"digest": "952c4c9bf24e001e23a33ebf97bd92969cd9143e28ce93f9aafc708a8f966903"
},
- {
- "name": "bathtub",
- "unicode": "1F6C1",
+ "bathtub": {
+ "category": "objects",
+ "moji": "🛁",
+ "unicodeVersion": "6.0",
"digest": "844dffb87ef872594195069b0d0df27c3fe51f3967ccbc8b2df811a086dd483a"
},
- {
- "name": "battery",
- "unicode": "1F50B",
+ "battery": {
+ "category": "objects",
+ "moji": "🔋",
+ "unicodeVersion": "6.0",
"digest": "949ae06648667fb13d9121a6dfdd03bf8692794b28c36e9a8e8ac4515664449a"
},
- {
- "name": "beach",
- "unicode": "1F3D6",
- "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
- },
- {
- "name": "beach_with_umbrella",
- "unicode": "1F3D6",
+ "beach": {
+ "category": "travel",
+ "moji": "🏖",
+ "unicodeVersion": "7.0",
"digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26"
},
- {
- "name": "beach_umbrella",
- "unicode": "26F1",
+ "beach_umbrella": {
+ "category": "objects",
+ "moji": "⛱",
+ "unicodeVersion": "5.2",
"digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
},
- {
- "name": "umbrella_on_ground",
- "unicode": "26F1",
- "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f"
- },
- {
- "name": "bear",
- "unicode": "1F43B",
+ "bear": {
+ "category": "nature",
+ "moji": "🐻",
+ "unicodeVersion": "6.0",
"digest": "a4b9066eaa5681e6af06e596a96a5217037460ffc3b013e8db4d34d762413246"
},
- {
- "name": "bed",
- "unicode": "1F6CF",
+ "bed": {
+ "category": "objects",
+ "moji": "🛏",
+ "unicodeVersion": "7.0",
"digest": "08f6e20db51b1fb650b390a0a3074938646772f3fcee8c295d47742e44fe1e30"
},
- {
- "name": "bee",
- "unicode": "1F41D",
+ "bee": {
+ "category": "nature",
+ "moji": "🐝",
+ "unicodeVersion": "6.0",
"digest": "5beb9a1650681b4adf69999d4808231c38f41a3ec693480b807cda86f964c570"
},
- {
- "name": "beer",
- "unicode": "1F37A",
+ "beer": {
+ "category": "food",
+ "moji": "🍺",
+ "unicodeVersion": "6.0",
"digest": "69e227104976548ee0f37375fe1526fd65ef0a328d2d92db2feb1edfd7032bd4"
},
- {
- "name": "beers",
- "unicode": "1F37B",
+ "beers": {
+ "category": "food",
+ "moji": "🍻",
+ "unicodeVersion": "6.0",
"digest": "db8b32d93bf6d161a3b027e55651d8f51231b13928b3610987ef62bb634d7501"
},
- {
- "name": "beetle",
- "unicode": "1F41E",
+ "beetle": {
+ "category": "nature",
+ "moji": "🐞",
+ "unicodeVersion": "6.0",
"digest": "5aaa428e3f63f7cd1696839ab05be03fa0cd0cbed30a05c36cb270da330c3849"
},
- {
- "name": "beginner",
- "unicode": "1F530",
+ "beginner": {
+ "category": "symbols",
+ "moji": "🔰",
+ "unicodeVersion": "6.0",
"digest": "2de4fdf92f182c42b12b7527034eaf767d996848b61f31ee69167728411ca0b1"
},
- {
- "name": "bell",
- "unicode": "1F514",
+ "bell": {
+ "category": "symbols",
+ "moji": "🔔",
+ "unicodeVersion": "6.0",
"digest": "18d419417746ead408072b78fe2edb6314cdb49492873966fa9f9f06be09899b"
},
- {
- "name": "bellhop",
- "unicode": "1F6CE",
- "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
- },
- {
- "name": "bellhop_bell",
- "unicode": "1F6CE",
+ "bellhop": {
+ "category": "objects",
+ "moji": "🛎",
+ "unicodeVersion": "7.0",
"digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08"
},
- {
- "name": "bento",
- "unicode": "1F371",
+ "bento": {
+ "category": "food",
+ "moji": "🍱",
+ "unicodeVersion": "6.0",
"digest": "d46d4f681c5da7f7678b51be3445454a8ed18d917e132ae79077f05310e485f1"
},
- {
- "name": "bicyclist",
- "unicode": "1F6B4",
+ "bicyclist": {
+ "category": "activity",
+ "moji": "🚴",
+ "unicodeVersion": "6.0",
"digest": "3302147b6b47c16adb97d78b7b761a1ca80e6d0b41d0b60f4da338d2f55f968b"
},
- {
- "name": "bicyclist_tone1",
- "unicode": "1F6B4-1F3FB",
+ "bicyclist_tone1": {
+ "category": "activity",
+ "moji": "🚴🏻",
+ "unicodeVersion": "8.0",
"digest": "27eaae0eb61f5e7b3cd9faf02c042d6643a368051a7c9d7da4e0fb9802d39242"
},
- {
- "name": "bicyclist_tone2",
- "unicode": "1F6B4-1F3FC",
+ "bicyclist_tone2": {
+ "category": "activity",
+ "moji": "🚴🏼",
+ "unicodeVersion": "8.0",
"digest": "39ee9e1071700da7079ad0146bf5711c3a222991eeca8b29b72a65677604444d"
},
- {
- "name": "bicyclist_tone3",
- "unicode": "1F6B4-1F3FD",
+ "bicyclist_tone3": {
+ "category": "activity",
+ "moji": "🚴🏽",
+ "unicodeVersion": "8.0",
"digest": "03e1d2c4232c896147a9d4bf43becd61edbb5c84fc7193ecea474c0f9fb36817"
},
- {
- "name": "bicyclist_tone4",
- "unicode": "1F6B4-1F3FE",
+ "bicyclist_tone4": {
+ "category": "activity",
+ "moji": "🚴🏾",
+ "unicodeVersion": "8.0",
"digest": "61393d9c4805be0379d86dd5bec9a1b02314433ab36cfd85bb48dfd073746617"
},
- {
- "name": "bicyclist_tone5",
- "unicode": "1F6B4-1F3FF",
+ "bicyclist_tone5": {
+ "category": "activity",
+ "moji": "🚴🏿",
+ "unicodeVersion": "8.0",
"digest": "2b46d5f8303e5710dbf5db3a4edc9d88a032fe123fe79158024c9f51df5458c6"
},
- {
- "name": "bike",
- "unicode": "1F6B2",
+ "bike": {
+ "category": "travel",
+ "moji": "🚲",
+ "unicodeVersion": "6.0",
"digest": "b41daa7c549d483e2336186a28baaa8ecb11986f490c0c54c793c44900c8f652"
},
- {
- "name": "bikini",
- "unicode": "1F459",
+ "bikini": {
+ "category": "people",
+ "moji": "👙",
+ "unicodeVersion": "6.0",
"digest": "07fe156f64673818d69ce3bf03950ca59e3b5d346e45ca541da4078ab791f5ae"
},
- {
- "name": "biohazard",
- "unicode": "2623",
- "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
- },
- {
- "name": "biohazard_sign",
- "unicode": "2623",
+ "biohazard": {
+ "category": "symbols",
+ "moji": "☣",
+ "unicodeVersion": "1.1",
"digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788"
},
- {
- "name": "bird",
- "unicode": "1F426",
+ "bird": {
+ "category": "nature",
+ "moji": "🐦",
+ "unicodeVersion": "6.0",
"digest": "f916eaf8f271b3767ade9eabb69594c0479f45472d471cabaf59f6e965c161e0"
},
- {
- "name": "birthday",
- "unicode": "1F382",
+ "birthday": {
+ "category": "food",
+ "moji": "🎂",
+ "unicodeVersion": "6.0",
"digest": "89e7c4c598ebee8ec8ab11ebe4ccc6defb7c4d2987ee2379a19b3b59827dd98a"
},
- {
- "name": "black_circle",
- "unicode": "26AB",
+ "black_circle": {
+ "category": "symbols",
+ "moji": "⚫",
+ "unicodeVersion": "4.1",
"digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
},
- {
- "name": "black_heart",
- "unicode": "1F5A4",
+ "black_heart": {
+ "category": "symbols",
+ "moji": "🖤",
+ "unicodeVersion": "9.0",
"digest": "f334679168d6dd7328c28e9ae3cb2b1fca0e9c2777938d586bfe623db2a688b9"
},
- {
- "name": "black_joker",
- "unicode": "1F0CF",
+ "black_joker": {
+ "category": "symbols",
+ "moji": "🃏",
+ "unicodeVersion": "6.0",
"digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d"
},
- {
- "name": "black_large_square",
- "unicode": "2B1B",
+ "black_large_square": {
+ "category": "symbols",
+ "moji": "⬛",
+ "unicodeVersion": "5.1",
"digest": "cbd90dcbc2f674eafa53820548b5263c18c9845ab39937f085e85aca0aebb479"
},
- {
- "name": "black_medium_small_square",
- "unicode": "25FE",
+ "black_medium_small_square": {
+ "category": "symbols",
+ "moji": "◾",
+ "unicodeVersion": "3.2",
"digest": "ab38363c2e862b8f67c719397a09a18e1ef996eec190691fdf769f5cfb209660"
},
- {
- "name": "black_medium_square",
- "unicode": "25FC",
+ "black_medium_square": {
+ "category": "symbols",
+ "moji": "◼",
+ "unicodeVersion": "3.2",
"digest": "c9ffa87c37e8ee65fadcf755176949901aec7367e02abb85e63cad60cd922116"
},
- {
- "name": "black_nib",
- "unicode": "2712",
+ "black_nib": {
+ "category": "objects",
+ "moji": "✒",
+ "unicodeVersion": "1.1",
"digest": "58fb23b1155102970eaa23765e7d529a21e8e545e076ec1158bf11b4de5f51a8"
},
- {
- "name": "black_small_square",
- "unicode": "25AA",
+ "black_small_square": {
+ "category": "symbols",
+ "moji": "▪",
+ "unicodeVersion": "1.1",
"digest": "f69be6de578fffce5a3e60eda690104b2ef6a855c630040104fb760a02ff1aef"
},
- {
- "name": "black_square_button",
- "unicode": "1F532",
+ "black_square_button": {
+ "category": "symbols",
+ "moji": "🔲",
+ "unicodeVersion": "6.0",
"digest": "9d818fcd08ed38cd0bbbcfd83e665aa29b3761c0d8b9806d8954d36785e267a8"
},
- {
- "name": "blossom",
- "unicode": "1F33C",
+ "blossom": {
+ "category": "nature",
+ "moji": "🌼",
+ "unicodeVersion": "6.0",
"digest": "e8cf369d4e4cdb4eccc2ebcbb35439b0344221115701daae642e58dff8544922"
},
- {
- "name": "blowfish",
- "unicode": "1F421",
+ "blowfish": {
+ "category": "nature",
+ "moji": "🐡",
+ "unicodeVersion": "6.0",
"digest": "e706849ed00f08a82312381c76f6f9ba6cc261fbf87a839c85e7dd54138f9dc3"
},
- {
- "name": "blue_book",
- "unicode": "1F4D8",
+ "blue_book": {
+ "category": "objects",
+ "moji": "📘",
+ "unicodeVersion": "6.0",
"digest": "4c845748fe890516b32981b0b62bf3e8e9d906840c2060179f4f844100780615"
},
- {
- "name": "blue_car",
- "unicode": "1F699",
+ "blue_car": {
+ "category": "travel",
+ "moji": "🚙",
+ "unicodeVersion": "6.0",
"digest": "eca91934eb5481726cfd897b1ed5eac306e14d02499fbe49316aaec6c72b6707"
},
- {
- "name": "blue_heart",
- "unicode": "1F499",
+ "blue_heart": {
+ "category": "symbols",
+ "moji": "💙",
+ "unicodeVersion": "6.0",
"digest": "2caa0c8d18538cc871c6fe328a52f71e1df8aabf4d1cc2f5324b261d1b8cb99a"
},
- {
- "name": "blush",
- "unicode": "1F60A",
+ "blush": {
+ "category": "people",
+ "moji": "😊",
+ "unicodeVersion": "6.0",
"digest": "3bfe8d603cfa39999c164779f666d39bbc507f124ba80233ee72da7b3b0c0457"
},
- {
- "name": "boar",
- "unicode": "1F417",
+ "boar": {
+ "category": "nature",
+ "moji": "🐗",
+ "unicodeVersion": "6.0",
"digest": "c9d67479cace427ac3c30460fcffa1bf9a8e5262c0390962405dbbe6bf830fa6"
},
- {
- "name": "bomb",
- "unicode": "1F4A3",
+ "bomb": {
+ "category": "objects",
+ "moji": "💣",
+ "unicodeVersion": "6.0",
"digest": "0155559abc4084f80e9b0b2a2091b8710ddd6369993b7fdd0685f4f8c2fd7e6c"
},
- {
- "name": "book",
- "unicode": "1F4D6",
+ "book": {
+ "category": "objects",
+ "moji": "📖",
+ "unicodeVersion": "6.0",
"digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf"
},
- {
- "name": "bookmark",
- "unicode": "1F516",
+ "bookmark": {
+ "category": "objects",
+ "moji": "🔖",
+ "unicodeVersion": "6.0",
"digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d"
},
- {
- "name": "bookmark_tabs",
- "unicode": "1F4D1",
+ "bookmark_tabs": {
+ "category": "objects",
+ "moji": "📑",
+ "unicodeVersion": "6.0",
"digest": "c8fc7c9f3f82e1ccc97fc591345fdd88b09eec0fca428d8d4632a121cf1bc39a"
},
- {
- "name": "books",
- "unicode": "1F4DA",
+ "books": {
+ "category": "objects",
+ "moji": "📚",
+ "unicodeVersion": "6.0",
"digest": "cbcf55d39dd05d26ef7350bc51e0e2f064f78bb8f59d407b516d63f68558f8e4"
},
- {
- "name": "boom",
- "unicode": "1F4A5",
+ "boom": {
+ "category": "nature",
+ "moji": "💥",
+ "unicodeVersion": "6.0",
"digest": "f5400e9583f7f997cd2385f21379f6229424a9b221445bc8f36c0bb64bdb3168"
},
- {
- "name": "boot",
- "unicode": "1F462",
+ "boot": {
+ "category": "people",
+ "moji": "👢",
+ "unicodeVersion": "6.0",
"digest": "b4706ff35909a6fb759a3b8a797e90cb67ffc60e4853386a7d89ace9693a9364"
},
- {
- "name": "bouquet",
- "unicode": "1F490",
+ "bouquet": {
+ "category": "nature",
+ "moji": "💐",
+ "unicodeVersion": "6.0",
"digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f"
},
- {
- "name": "bow",
- "unicode": "1F647",
+ "bow": {
+ "category": "people",
+ "moji": "🙇",
+ "unicodeVersion": "6.0",
"digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd"
},
- {
- "name": "bow_and_arrow",
- "unicode": "1F3F9",
- "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
- },
- {
- "name": "archery",
- "unicode": "1F3F9",
+ "bow_and_arrow": {
+ "category": "activity",
+ "moji": "🏹",
+ "unicodeVersion": "8.0",
"digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d"
},
- {
- "name": "bow_tone1",
- "unicode": "1F647-1F3FB",
+ "bow_tone1": {
+ "category": "people",
+ "moji": "🙇🏻",
+ "unicodeVersion": "8.0",
"digest": "995c8400ad60d5adc66c9ae5e3c0ecf56c48b478ad79418d45b6289933d25bdd"
},
- {
- "name": "bow_tone2",
- "unicode": "1F647-1F3FC",
+ "bow_tone2": {
+ "category": "people",
+ "moji": "🙇🏼",
+ "unicodeVersion": "8.0",
"digest": "af89eec2fccda99d9bdd373b2345595882fee1c0a15d29af9028089e20255325"
},
- {
- "name": "bow_tone3",
- "unicode": "1F647-1F3FD",
+ "bow_tone3": {
+ "category": "people",
+ "moji": "🙇🏽",
+ "unicodeVersion": "8.0",
"digest": "015d8122abdf2d0caa03815545f50fb7a71e05dacd46aaa133cc9ace5192f266"
},
- {
- "name": "bow_tone4",
- "unicode": "1F647-1F3FE",
+ "bow_tone4": {
+ "category": "people",
+ "moji": "🙇🏾",
+ "unicodeVersion": "8.0",
"digest": "e8409096a795b775def654d36aeccb8eb91e83d7d1b32145cd73fd0b7b9e885c"
},
- {
- "name": "bow_tone5",
- "unicode": "1F647-1F3FF",
+ "bow_tone5": {
+ "category": "people",
+ "moji": "🙇🏿",
+ "unicodeVersion": "8.0",
"digest": "d87042cde8dbad9fb1a91a2ec60116e27b4a76388b5779d771a0bbae12a2814d"
},
- {
- "name": "bowling",
- "unicode": "1F3B3",
+ "bowling": {
+ "category": "activity",
+ "moji": "🎳",
+ "unicodeVersion": "6.0",
"digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662"
},
- {
- "name": "boxing_glove",
- "unicode": "1F94A",
+ "boxing_glove": {
+ "category": "activity",
+ "moji": "🥊",
+ "unicodeVersion": "9.0",
"digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
},
- {
- "name": "boxing_gloves",
- "unicode": "1F94A",
- "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
- },
- {
- "name": "boy",
- "unicode": "1F466",
+ "boy": {
+ "category": "people",
+ "moji": "👦",
+ "unicodeVersion": "6.0",
"digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1"
},
- {
- "name": "boy_tone1",
- "unicode": "1F466-1F3FB",
+ "boy_tone1": {
+ "category": "people",
+ "moji": "👦🏻",
+ "unicodeVersion": "8.0",
"digest": "c0e2f0483715b239fe145b0056566f7a3a722319d9a87c1e66733dff1916a19f"
},
- {
- "name": "boy_tone2",
- "unicode": "1F466-1F3FC",
+ "boy_tone2": {
+ "category": "people",
+ "moji": "👦🏼",
+ "unicodeVersion": "8.0",
"digest": "0001d0bd1ff4dbd898604ba965b4039d09667d955bc0349301b992f9ab6dd7fd"
},
- {
- "name": "boy_tone3",
- "unicode": "1F466-1F3FD",
+ "boy_tone3": {
+ "category": "people",
+ "moji": "👦🏽",
+ "unicodeVersion": "8.0",
"digest": "e0f08755955fd2e0bd1c5d5e84429b2a234b24a744bb50bb9f1148495b2b29f9"
},
- {
- "name": "boy_tone4",
- "unicode": "1F466-1F3FE",
+ "boy_tone4": {
+ "category": "people",
+ "moji": "👦🏾",
+ "unicodeVersion": "8.0",
"digest": "04b6bfee58a26b1ce2e5b403504a7033aaf395f03f5cd23e824f32c90c395fe6"
},
- {
- "name": "boy_tone5",
- "unicode": "1F466-1F3FF",
+ "boy_tone5": {
+ "category": "people",
+ "moji": "👦🏿",
+ "unicodeVersion": "8.0",
"digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5"
},
- {
- "name": "bread",
- "unicode": "1F35E",
+ "bread": {
+ "category": "food",
+ "moji": "🍞",
+ "unicodeVersion": "6.0",
"digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86"
},
- {
- "name": "bride_with_veil",
- "unicode": "1F470",
+ "bride_with_veil": {
+ "category": "people",
+ "moji": "👰",
+ "unicodeVersion": "6.0",
"digest": "8e24bd91c3f564cf6148f2b3b4a7d692c11dd059e76a13331fdfb04ae060ea70"
},
- {
- "name": "bride_with_veil_tone1",
- "unicode": "1F470-1F3FB",
+ "bride_with_veil_tone1": {
+ "category": "people",
+ "moji": "👰🏻",
+ "unicodeVersion": "8.0",
"digest": "0bd2f16f72586f50e768b14b9b353f2e98ccbb2581a568c33b06be56e70ca063"
},
- {
- "name": "bride_with_veil_tone2",
- "unicode": "1F470-1F3FC",
+ "bride_with_veil_tone2": {
+ "category": "people",
+ "moji": "👰🏼",
+ "unicodeVersion": "8.0",
"digest": "e5463f811b2075754f0718b891757cd2e81071edf7af2215581227e1aad1d068"
},
- {
- "name": "bride_with_veil_tone3",
- "unicode": "1F470-1F3FD",
+ "bride_with_veil_tone3": {
+ "category": "people",
+ "moji": "👰🏽",
+ "unicodeVersion": "8.0",
"digest": "e5a053a26f7ccebae7eb12f638be5ed80f77b744708d783eab2eb8aa091cf516"
},
- {
- "name": "bride_with_veil_tone4",
- "unicode": "1F470-1F3FE",
+ "bride_with_veil_tone4": {
+ "category": "people",
+ "moji": "👰🏾",
+ "unicodeVersion": "8.0",
"digest": "410e23825e4401460946dc67a618bd3ace6e1a7c07dd88580a2349423685261f"
},
- {
- "name": "bride_with_veil_tone5",
- "unicode": "1F470-1F3FF",
+ "bride_with_veil_tone5": {
+ "category": "people",
+ "moji": "👰🏿",
+ "unicodeVersion": "8.0",
"digest": "454e87e5a74e13e5b4993541231516fbbe6dbe9f990e1a6f3f4a744d7d4c1615"
},
- {
- "name": "bridge_at_night",
- "unicode": "1F309",
+ "bridge_at_night": {
+ "category": "travel",
+ "moji": "🌉",
+ "unicodeVersion": "6.0",
"digest": "9d3cda5a59e27e3c90939f1ddbe7e998b3ea4fcacfa1467dea0edf39613c2d7f"
},
- {
- "name": "briefcase",
- "unicode": "1F4BC",
+ "briefcase": {
+ "category": "people",
+ "moji": "💼",
+ "unicodeVersion": "6.0",
"digest": "9d00d6a92632aaadc71b017f448c883b27eb31a7554ebb51f7e3a9841f0f7f2b"
},
- {
- "name": "broken_heart",
- "unicode": "1F494",
+ "broken_heart": {
+ "category": "symbols",
+ "moji": "💔",
+ "unicodeVersion": "6.0",
"digest": "c7ca53f444d72e596af46b61ffbc9e7c18a645020c22691e44f967db98dbf853"
},
- {
- "name": "bug",
- "unicode": "1F41B",
+ "bug": {
+ "category": "nature",
+ "moji": "🐛",
+ "unicodeVersion": "6.0",
"digest": "0dccb1d5eb91769377b4c5b310f007b60f54a5c48ba9e467b3a06898a4831b90"
},
- {
- "name": "bulb",
- "unicode": "1F4A1",
+ "bulb": {
+ "category": "objects",
+ "moji": "💡",
+ "unicodeVersion": "6.0",
"digest": "ccdaa2dfde5a88a347035a94b9d4d86cfc335ce0a73292423f5788a4bd21a5a8"
},
- {
- "name": "bullettrain_front",
- "unicode": "1F685",
+ "bullettrain_front": {
+ "category": "travel",
+ "moji": "🚅",
+ "unicodeVersion": "6.0",
"digest": "5195a6a6d23f28e1aa5ebac6ede0f6c6a8b7ff33a9edf034814f227fe976177a"
},
- {
- "name": "bullettrain_side",
- "unicode": "1F684",
+ "bullettrain_side": {
+ "category": "travel",
+ "moji": "🚄",
+ "unicodeVersion": "6.0",
"digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7"
},
- {
- "name": "burrito",
- "unicode": "1F32F",
+ "burrito": {
+ "category": "food",
+ "moji": "🌯",
+ "unicodeVersion": "8.0",
"digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf"
},
- {
- "name": "bus",
- "unicode": "1F68C",
+ "bus": {
+ "category": "travel",
+ "moji": "🚌",
+ "unicodeVersion": "6.0",
"digest": "192850b762edad21ac8770df38b9cae6d2bc1697a838462f3e36066bfb4eee50"
},
- {
- "name": "busstop",
- "unicode": "1F68F",
+ "busstop": {
+ "category": "travel",
+ "moji": "🚏",
+ "unicodeVersion": "6.0",
"digest": "adabb1ec36402b33feb636eae3656e5a8b51ff1071bcb14125d8ab80d6d12d2a"
},
- {
- "name": "bust_in_silhouette",
- "unicode": "1F464",
+ "bust_in_silhouette": {
+ "category": "people",
+ "moji": "👤",
+ "unicodeVersion": "6.0",
"digest": "277ae43301f1e49e0be03c8e52f0dc7b70c67f9d146bca0a14172e0098f115e6"
},
- {
- "name": "busts_in_silhouette",
- "unicode": "1F465",
+ "busts_in_silhouette": {
+ "category": "people",
+ "moji": "👥",
+ "unicodeVersion": "6.0",
"digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b"
},
- {
- "name": "butterfly",
- "unicode": "1F98B",
+ "butterfly": {
+ "category": "nature",
+ "moji": "🦋",
+ "unicodeVersion": "9.0",
"digest": "a91b6598c17b44a8dc8935a1d99e25f4483ea41470cdd2da343039a9eec29ef1"
},
- {
- "name": "cactus",
- "unicode": "1F335",
+ "cactus": {
+ "category": "nature",
+ "moji": "🌵",
+ "unicodeVersion": "6.0",
"digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd"
},
- {
- "name": "cake",
- "unicode": "1F370",
+ "cake": {
+ "category": "food",
+ "moji": "🍰",
+ "unicodeVersion": "6.0",
"digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b"
},
- {
- "name": "calendar",
- "unicode": "1F4C6",
+ "calendar": {
+ "category": "objects",
+ "moji": "📆",
+ "unicodeVersion": "6.0",
"digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3"
},
- {
- "name": "calendar_spiral",
- "unicode": "1F5D3",
- "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
- },
- {
- "name": "spiral_calendar_pad",
- "unicode": "1F5D3",
+ "calendar_spiral": {
+ "category": "objects",
+ "moji": "🗓",
+ "unicodeVersion": "7.0",
"digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
},
- {
- "name": "call_me",
- "unicode": "1F919",
- "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
- },
- {
- "name": "call_me_hand",
- "unicode": "1F919",
+ "call_me": {
+ "category": "people",
+ "moji": "🤙",
+ "unicodeVersion": "9.0",
"digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
},
- {
- "name": "call_me_tone1",
- "unicode": "1F919-1F3FB",
+ "call_me_tone1": {
+ "category": "people",
+ "moji": "🤙🏻",
+ "unicodeVersion": "9.0",
"digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
},
- {
- "name": "call_me_hand_tone1",
- "unicode": "1F919-1F3FB",
- "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
- },
- {
- "name": "call_me_tone2",
- "unicode": "1F919-1F3FC",
- "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
- },
- {
- "name": "call_me_hand_tone2",
- "unicode": "1F919-1F3FC",
+ "call_me_tone2": {
+ "category": "people",
+ "moji": "🤙🏼",
+ "unicodeVersion": "9.0",
"digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
},
- {
- "name": "call_me_tone3",
- "unicode": "1F919-1F3FD",
+ "call_me_tone3": {
+ "category": "people",
+ "moji": "🤙🏽",
+ "unicodeVersion": "9.0",
"digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
},
- {
- "name": "call_me_hand_tone3",
- "unicode": "1F919-1F3FD",
- "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
- },
- {
- "name": "call_me_tone4",
- "unicode": "1F919-1F3FE",
- "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
- },
- {
- "name": "call_me_hand_tone4",
- "unicode": "1F919-1F3FE",
+ "call_me_tone4": {
+ "category": "people",
+ "moji": "🤙🏾",
+ "unicodeVersion": "9.0",
"digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
},
- {
- "name": "call_me_tone5",
- "unicode": "1F919-1F3FF",
+ "call_me_tone5": {
+ "category": "people",
+ "moji": "🤙🏿",
+ "unicodeVersion": "9.0",
"digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
},
- {
- "name": "call_me_hand_tone5",
- "unicode": "1F919-1F3FF",
- "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
- },
- {
- "name": "calling",
- "unicode": "1F4F2",
+ "calling": {
+ "category": "objects",
+ "moji": "📲",
+ "unicodeVersion": "6.0",
"digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91"
},
- {
- "name": "camel",
- "unicode": "1F42B",
+ "camel": {
+ "category": "nature",
+ "moji": "🐫",
+ "unicodeVersion": "6.0",
"digest": "5f927927a7ab1277d0dc8b8211436957968b1e11365a8bf535e9bb94f92c5631"
},
- {
- "name": "camera",
- "unicode": "1F4F7",
+ "camera": {
+ "category": "objects",
+ "moji": "📷",
+ "unicodeVersion": "6.0",
"digest": "fde03e396822a36cd6ae756ede885b945a074395264162731ca5db47a3b39d80"
},
- {
- "name": "camera_with_flash",
- "unicode": "1F4F8",
+ "camera_with_flash": {
+ "category": "objects",
+ "moji": "📸",
+ "unicodeVersion": "7.0",
"digest": "9afd380208187780f00244c45d4db6c5ea1ea088d4a1bd8fc92a8f3877149750"
},
- {
- "name": "camping",
- "unicode": "1F3D5",
+ "camping": {
+ "category": "travel",
+ "moji": "🏕",
+ "unicodeVersion": "7.0",
"digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9"
},
- {
- "name": "cancer",
- "unicode": "264B",
+ "cancer": {
+ "category": "symbols",
+ "moji": "♋",
+ "unicodeVersion": "1.1",
"digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6"
},
- {
- "name": "candle",
- "unicode": "1F56F",
+ "candle": {
+ "category": "objects",
+ "moji": "🕯",
+ "unicodeVersion": "7.0",
"digest": "211c04dc3a91b071c284d4180ed09f9d3320e3fd6ba8a9fddd0677bc97fd12cb"
},
- {
- "name": "candy",
- "unicode": "1F36C",
+ "candy": {
+ "category": "food",
+ "moji": "🍬",
+ "unicodeVersion": "6.0",
"digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100"
},
- {
- "name": "canoe",
- "unicode": "1F6F6",
- "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
- },
- {
- "name": "kayak",
- "unicode": "1F6F6",
+ "canoe": {
+ "category": "travel",
+ "moji": "🛶",
+ "unicodeVersion": "9.0",
"digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
},
- {
- "name": "capital_abcd",
- "unicode": "1F520",
+ "capital_abcd": {
+ "category": "symbols",
+ "moji": "🔠",
+ "unicodeVersion": "6.0",
"digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa"
},
- {
- "name": "capricorn",
- "unicode": "2651",
+ "capricorn": {
+ "category": "symbols",
+ "moji": "♑",
+ "unicodeVersion": "1.1",
"digest": "f11abad102603737b55486fe2ea4d01f28b203394bcd84f19a7948156e6c4b96"
},
- {
- "name": "card_box",
- "unicode": "1F5C3",
+ "card_box": {
+ "category": "objects",
+ "moji": "🗃",
+ "unicodeVersion": "7.0",
"digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
},
- {
- "name": "card_file_box",
- "unicode": "1F5C3",
- "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a"
- },
- {
- "name": "card_index",
- "unicode": "1F4C7",
+ "card_index": {
+ "category": "objects",
+ "moji": "📇",
+ "unicodeVersion": "6.0",
"digest": "86e187e0a72ca5d00207d6ef34d66ce15046848a831c2b5184fb840c5332a2a8"
},
- {
- "name": "carousel_horse",
- "unicode": "1F3A0",
+ "carousel_horse": {
+ "category": "travel",
+ "moji": "🎠",
+ "unicodeVersion": "6.0",
"digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe"
},
- {
- "name": "carrot",
- "unicode": "1F955",
+ "carrot": {
+ "category": "food",
+ "moji": "🥕",
+ "unicodeVersion": "9.0",
"digest": "3a6fd98b63ee73d982a9cdacb08cf7b4014368cde8ffce6056b7df25a5a472b1"
},
- {
- "name": "cartwheel",
- "unicode": "1F938",
- "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
- },
- {
- "name": "person_doing_cartwheel",
- "unicode": "1F938",
+ "cartwheel": {
+ "category": "activity",
+ "moji": "🤸",
+ "unicodeVersion": "9.0",
"digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
},
- {
- "name": "cartwheel_tone1",
- "unicode": "1F938-1F3FB",
+ "cartwheel_tone1": {
+ "category": "activity",
+ "moji": "🤸🏻",
+ "unicodeVersion": "9.0",
"digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
},
- {
- "name": "person_doing_cartwheel_tone1",
- "unicode": "1F938-1F3FB",
- "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
- },
- {
- "name": "cartwheel_tone2",
- "unicode": "1F938-1F3FC",
- "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
- },
- {
- "name": "person_doing_cartwheel_tone2",
- "unicode": "1F938-1F3FC",
+ "cartwheel_tone2": {
+ "category": "activity",
+ "moji": "🤸🏼",
+ "unicodeVersion": "9.0",
"digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
},
- {
- "name": "cartwheel_tone3",
- "unicode": "1F938-1F3FD",
+ "cartwheel_tone3": {
+ "category": "activity",
+ "moji": "🤸🏽",
+ "unicodeVersion": "9.0",
"digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
},
- {
- "name": "person_doing_cartwheel_tone3",
- "unicode": "1F938-1F3FD",
- "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
- },
- {
- "name": "cartwheel_tone4",
- "unicode": "1F938-1F3FE",
- "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
- },
- {
- "name": "person_doing_cartwheel_tone4",
- "unicode": "1F938-1F3FE",
+ "cartwheel_tone4": {
+ "category": "activity",
+ "moji": "🤸🏾,",
+ "unicodeVersion": "9.0",
"digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
},
- {
- "name": "cartwheel_tone5",
- "unicode": "1F938-1F3FF",
+ "cartwheel_tone5": {
+ "category": "activity",
+ "moji": "🤸🏿",
+ "unicodeVersion": "9.0",
"digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
},
- {
- "name": "person_doing_cartwheel_tone5",
- "unicode": "1F938-1F3FF",
- "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
- },
- {
- "name": "cat",
- "unicode": "1F431",
+ "cat": {
+ "category": "nature",
+ "moji": "🐱",
+ "unicodeVersion": "6.0",
"digest": "e52d0d3a205a0ba99094717e171a7f572b713a0e21b276ffa4a826596fe5cafc"
},
- {
- "name": "cat2",
- "unicode": "1F408",
+ "cat2": {
+ "category": "nature",
+ "moji": "🐈",
+ "unicodeVersion": "6.0",
"digest": "46aa67a99f782935932c77b8de93287142297abe52928c173191cf55bb8f4339"
},
- {
- "name": "cd",
- "unicode": "1F4BF",
+ "cd": {
+ "category": "objects",
+ "moji": "💿",
+ "unicodeVersion": "6.0",
"digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b"
},
- {
- "name": "chains",
- "unicode": "26D3",
+ "chains": {
+ "category": "objects",
+ "moji": "⛓",
+ "unicodeVersion": "5.2",
"digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2"
},
- {
- "name": "champagne",
- "unicode": "1F37E",
- "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
- },
- {
- "name": "bottle_with_popping_cork",
- "unicode": "1F37E",
+ "champagne": {
+ "category": "food",
+ "moji": "🍾",
+ "unicodeVersion": "8.0",
"digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
},
- {
- "name": "champagne_glass",
- "unicode": "1F942",
+ "champagne_glass": {
+ "category": "food",
+ "moji": "🥂",
+ "unicodeVersion": "9.0",
"digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
},
- {
- "name": "clinking_glass",
- "unicode": "1F942",
- "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
- },
- {
- "name": "chart",
- "unicode": "1F4B9",
+ "chart": {
+ "category": "symbols",
+ "moji": "💹",
+ "unicodeVersion": "6.0",
"digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f"
},
- {
- "name": "chart_with_downwards_trend",
- "unicode": "1F4C9",
+ "chart_with_downwards_trend": {
+ "category": "objects",
+ "moji": "📉",
+ "unicodeVersion": "6.0",
"digest": "5db7ccbc37665736a9c0b2f50247dcc09e404ec37f39db45b7b8b9464172a18c"
},
- {
- "name": "chart_with_upwards_trend",
- "unicode": "1F4C8",
+ "chart_with_upwards_trend": {
+ "category": "objects",
+ "moji": "📈",
+ "unicodeVersion": "6.0",
"digest": "bc4ea250b102fe5c09847e471478aff065ad3df755d9717896d38d887d9c6733"
},
- {
- "name": "checkered_flag",
- "unicode": "1F3C1",
+ "checkered_flag": {
+ "category": "travel",
+ "moji": "🏁",
+ "unicodeVersion": "6.0",
"digest": "0e77180e0cf9fc87e755a5a42cf23aec6bf30931db41331311e97ba0be178b78"
},
- {
- "name": "cheese",
- "unicode": "1F9C0",
+ "cheese": {
+ "category": "food",
+ "moji": "🧀",
+ "unicodeVersion": "8.0",
"digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
},
- {
- "name": "cheese_wedge",
- "unicode": "1F9C0",
- "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b"
- },
- {
- "name": "cherries",
- "unicode": "1F352",
+ "cherries": {
+ "category": "food",
+ "moji": "🍒",
+ "unicodeVersion": "6.0",
"digest": "13b8db9e7e6eec8509aa80c762966e1bf3538fcb1ac3d6eab18ee4da1528cf84"
},
- {
- "name": "cherry_blossom",
- "unicode": "1F338",
+ "cherry_blossom": {
+ "category": "nature",
+ "moji": "🌸",
+ "unicodeVersion": "6.0",
"digest": "af3083f5f8dd94936113f2e16caba5aec7a774d5589aa08bf5de82a2d278cc66"
},
- {
- "name": "chestnut",
- "unicode": "1F330",
+ "chestnut": {
+ "category": "nature",
+ "moji": "🌰",
+ "unicodeVersion": "6.0",
"digest": "9f85b79b207a69ab81ab88dcef04954000965b039b4cf57de5f1b381745ab98b"
},
- {
- "name": "chicken",
- "unicode": "1F414",
+ "chicken": {
+ "category": "nature",
+ "moji": "🐔",
+ "unicodeVersion": "6.0",
"digest": "57ceb4459d183740009caac6ebed089d2f1e12f67c138e1be1d0f992313c0ac4"
},
- {
- "name": "children_crossing",
- "unicode": "1F6B8",
+ "children_crossing": {
+ "category": "symbols",
+ "moji": "🚸",
+ "unicodeVersion": "6.0",
"digest": "0ded7d9aca0161e8ef8e2858c3c198e70e4badc7105ac3a6886e06975de19106"
},
- {
- "name": "chipmunk",
- "unicode": "1F43F",
+ "chipmunk": {
+ "category": "nature",
+ "moji": "🐿",
+ "unicodeVersion": "7.0",
"digest": "5b0dc1a859163097727ba2ba5ffca38b0a54d925eebb089977d28d0b4d917a3f"
},
- {
- "name": "chocolate_bar",
- "unicode": "1F36B",
+ "chocolate_bar": {
+ "category": "food",
+ "moji": "🍫",
+ "unicodeVersion": "6.0",
"digest": "dd273e5050488acaf885f8a18b6e2b3901f69c5b39fa6465fb60621783d4109a"
},
- {
- "name": "christmas_tree",
- "unicode": "1F384",
+ "christmas_tree": {
+ "category": "nature",
+ "moji": "🎄",
+ "unicodeVersion": "6.0",
"digest": "ce60cbe2ebbe8057be8edea2392455fedd2bcda64a0a831f6a1942028af7e747"
},
- {
- "name": "church",
- "unicode": "26EA",
+ "church": {
+ "category": "travel",
+ "moji": "⛪",
+ "unicodeVersion": "5.2",
"digest": "2c328456528f7336e59443e20ec3ab22fe71f1fccb1dd50d0ad68eb206937557"
},
- {
- "name": "cinema",
- "unicode": "1F3A6",
+ "cinema": {
+ "category": "symbols",
+ "moji": "🎦",
+ "unicodeVersion": "6.0",
"digest": "4c26dcdc76f93dbc2a1dc49ed4e132b8e8f2b7cdc1acf5e09b3dfd99430d97cd"
},
- {
- "name": "circus_tent",
- "unicode": "1F3AA",
+ "circus_tent": {
+ "category": "activity",
+ "moji": "🎪",
+ "unicodeVersion": "6.0",
"digest": "fec5f2a06222be8be549178b29720343cc00145177ec387ca4e6f3432481fe77"
},
- {
- "name": "city_dusk",
- "unicode": "1F306",
+ "city_dusk": {
+ "category": "travel",
+ "moji": "🌆",
+ "unicodeVersion": "6.0",
"digest": "bba345e949dcc51f5f018220f000223797970c82ead2ab9c822f9dc0847aa155"
},
- {
- "name": "city_sunset",
- "unicode": "1F307",
+ "city_sunset": {
+ "category": "travel",
+ "moji": "🌇",
+ "unicodeVersion": "6.0",
"digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
},
- {
- "name": "city_sunrise",
- "unicode": "1F307",
- "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7"
- },
- {
- "name": "cityscape",
- "unicode": "1F3D9",
+ "cityscape": {
+ "category": "travel",
+ "moji": "🏙",
+ "unicodeVersion": "7.0",
"digest": "ee360be7514c4bfb0d539dd28f3b2031ebcef04e850723ec0685fb54bd8e6d5f"
},
- {
- "name": "cl",
- "unicode": "1F191",
+ "cl": {
+ "category": "symbols",
+ "moji": "🆑",
+ "unicodeVersion": "6.0",
"digest": "fcec2855dbad9fda11d6e2802bc0dcaabab0b5be233508f5e439f156f07602c1"
},
- {
- "name": "clap",
- "unicode": "1F44F",
+ "clap": {
+ "category": "people",
+ "moji": "👏",
+ "unicodeVersion": "6.0",
"digest": "a1860ce7812a9f6fb55e45761e1b79a2f8f0620eb04f80748a38420889d58a2a"
},
- {
- "name": "clap_tone1",
- "unicode": "1F44F-1F3FB",
+ "clap_tone1": {
+ "category": "people",
+ "moji": "👏🏻",
+ "unicodeVersion": "8.0",
"digest": "18a7022e08223fb2109af5a9b9a5b4f47dc870ce4453f4987d2d0b729ef54586"
},
- {
- "name": "clap_tone2",
- "unicode": "1F44F-1F3FC",
+ "clap_tone2": {
+ "category": "people",
+ "moji": "👏🏼",
+ "unicodeVersion": "8.0",
"digest": "5954c8658b15e755d2018d8674df84d38e22ffededc4d726c6a33b709f71426a"
},
- {
- "name": "clap_tone3",
- "unicode": "1F44F-1F3FD",
+ "clap_tone3": {
+ "category": "people",
+ "moji": "👏🏽",
+ "unicodeVersion": "8.0",
"digest": "22639b6bd3c53784a2f855d6db7bdf31621519f19dfc29a6bc310eee6421f742"
},
- {
- "name": "clap_tone4",
- "unicode": "1F44F-1F3FE",
+ "clap_tone4": {
+ "category": "people",
+ "moji": "👏🏾",
+ "unicodeVersion": "8.0",
"digest": "e55248dc163d1bbd118b50cd8767750ead86d082151febbc0a75b32d63abceec"
},
- {
- "name": "clap_tone5",
- "unicode": "1F44F-1F3FF",
+ "clap_tone5": {
+ "category": "people",
+ "moji": "👏🏿",
+ "unicodeVersion": "8.0",
"digest": "76046b8157dabbe048a07fc318122456020c9c980fc1b8ab76802330e07b3b53"
},
- {
- "name": "clapper",
- "unicode": "1F3AC",
+ "clapper": {
+ "category": "activity",
+ "moji": "🎬",
+ "unicodeVersion": "6.0",
"digest": "8149752a0e3e8abede2d433d1afab6d217877d0c76adb1e2845a0142c0cdcbaa"
},
- {
- "name": "classical_building",
- "unicode": "1F3DB",
+ "classical_building": {
+ "category": "travel",
+ "moji": "🏛",
+ "unicodeVersion": "7.0",
"digest": "9ee0d00c43d6e22b6a3ddea67619737270cc7e9294797a19c7c60d5f92aa44fa"
},
- {
- "name": "clipboard",
- "unicode": "1F4CB",
+ "clipboard": {
+ "category": "objects",
+ "moji": "📋",
+ "unicodeVersion": "6.0",
"digest": "bdd7f7d973c714e59d2903d401a876e6018794c7987c9ca57108c137c5edc25f"
},
- {
- "name": "clock",
- "unicode": "1F570",
- "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
- },
- {
- "name": "mantlepiece_clock",
- "unicode": "1F570",
+ "clock": {
+ "category": "objects",
+ "moji": "🕰",
+ "unicodeVersion": "7.0",
"digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190"
},
- {
- "name": "clock1",
- "unicode": "1F550",
+ "clock1": {
+ "category": "symbols",
+ "moji": "🕐",
+ "unicodeVersion": "6.0",
"digest": "1778eec07ce061c9393e5abee5ca83b24e1ce61d8a75fa2e39efcb31aa160395"
},
- {
- "name": "clock10",
- "unicode": "1F559",
+ "clock10": {
+ "category": "symbols",
+ "moji": "🕙",
+ "unicodeVersion": "6.0",
"digest": "601fc12ea5280a54c2e69dbb685f454e4165fe771756ed6f89016e29e683a24f"
},
- {
- "name": "clock1030",
- "unicode": "1F565",
+ "clock1030": {
+ "category": "symbols",
+ "moji": "🕥",
+ "unicodeVersion": "6.0",
"digest": "4fd155f08f797542d52cff4b0aa3ca9f080f37a41c301b82f90ff6d4693c890e"
},
- {
- "name": "clock11",
- "unicode": "1F55A",
+ "clock11": {
+ "category": "symbols",
+ "moji": "🕚",
+ "unicodeVersion": "6.0",
"digest": "5c79dc812e812e8a01993ea633b323d654ce3a7ea258692781a4896e4ad2017e"
},
- {
- "name": "clock1130",
- "unicode": "1F566",
+ "clock1130": {
+ "category": "symbols",
+ "moji": "🕦",
+ "unicodeVersion": "6.0",
"digest": "41497ee2020ee5ac9aa5f9b07560f7afca7c422b04214449cfc5cea9f020f52e"
},
- {
- "name": "clock12",
- "unicode": "1F55B",
+ "clock12": {
+ "category": "symbols",
+ "moji": "🕛",
+ "unicodeVersion": "6.0",
"digest": "046bb7ffa5f5d27c2e3411ba543484d9dabb8ebf6d6e7a7e9bfb088c1813500c"
},
- {
- "name": "clock1230",
- "unicode": "1F567",
+ "clock1230": {
+ "category": "symbols",
+ "moji": "🕧",
+ "unicodeVersion": "6.0",
"digest": "bbfe9db5a2043aaba19a7a2a0185c7efcebf1e8c9263b8233f75b53c4825f0f4"
},
- {
- "name": "clock130",
- "unicode": "1F55C",
+ "clock130": {
+ "category": "symbols",
+ "moji": "🕜",
+ "unicodeVersion": "6.0",
"digest": "8662cb395ee680c2781123305c4c8ce8c0df9565c2c942668940be540cc0c094"
},
- {
- "name": "clock2",
- "unicode": "1F551",
+ "clock2": {
+ "category": "symbols",
+ "moji": "🕑",
+ "unicodeVersion": "6.0",
"digest": "42f7429748b612dce7de77221cbbc710655811f7bb23e2a986c36e6d662f0ec4"
},
- {
- "name": "clock230",
- "unicode": "1F55D",
+ "clock230": {
+ "category": "symbols",
+ "moji": "🕝",
+ "unicodeVersion": "6.0",
"digest": "e710b6ef14227cd240ea3e2a867c8ef45b5c060adf3cb30ba9077c2351fe6677"
},
- {
- "name": "clock3",
- "unicode": "1F552",
+ "clock3": {
+ "category": "symbols",
+ "moji": "🕒",
+ "unicodeVersion": "6.0",
"digest": "7340d465b398a378211dff9ec806db579d061206fd6fc238623d070cfe0a55ce"
},
- {
- "name": "clock330",
- "unicode": "1F55E",
+ "clock330": {
+ "category": "symbols",
+ "moji": "🕞",
+ "unicodeVersion": "6.0",
"digest": "7aa4a15cc8de04ed3bdeb0f8a54a7915065f2809a07054e002d89926c9766831"
},
- {
- "name": "clock4",
- "unicode": "1F553",
+ "clock4": {
+ "category": "symbols",
+ "moji": "🕓",
+ "unicodeVersion": "6.0",
"digest": "36fd88e81ad488b0ec49a911a838693281573fa14736ae4a6dd1c40a4ff69bb1"
},
- {
- "name": "clock430",
- "unicode": "1F55F",
+ "clock430": {
+ "category": "symbols",
+ "moji": "🕟",
+ "unicodeVersion": "6.0",
"digest": "7bd5dd71e89d95dcf18b9e8c1fe2a353a7da3b69aadb8dda80ee9bafb05da58d"
},
- {
- "name": "clock5",
- "unicode": "1F554",
+ "clock5": {
+ "category": "symbols",
+ "moji": "🕔",
+ "unicodeVersion": "6.0",
"digest": "aa406409e56a0bfd8c850e44efe45fd190ffd7bf7061e934ed7928dfbdfc9eba"
},
- {
- "name": "clock530",
- "unicode": "1F560",
+ "clock530": {
+ "category": "symbols",
+ "moji": "🕠",
+ "unicodeVersion": "6.0",
"digest": "25dd3bcc53ddd98eeea498d7dbd4c306ef39dd033f15909063388a0800febf41"
},
- {
- "name": "clock6",
- "unicode": "1F555",
+ "clock6": {
+ "category": "symbols",
+ "moji": "🕕",
+ "unicodeVersion": "6.0",
"digest": "0a321eaf1bc5db8436bbadac66c45ba257fc98ad4c7569ce3fc6602c824b6d7c"
},
- {
- "name": "clock630",
- "unicode": "1F561",
+ "clock630": {
+ "category": "symbols",
+ "moji": "🕡",
+ "unicodeVersion": "6.0",
"digest": "55a4c5a665fdd38a724e9357a93c55401fcd5f1b13078c25754bd70c3fc4ccec"
},
- {
- "name": "clock7",
- "unicode": "1F556",
+ "clock7": {
+ "category": "symbols",
+ "moji": "🕖",
+ "unicodeVersion": "6.0",
"digest": "6154306545716e865da0ec537ee4f22bfe6c7294502a64a2dcf425c587d0e2a2"
},
- {
- "name": "clock730",
- "unicode": "1F562",
+ "clock730": {
+ "category": "symbols",
+ "moji": "🕢",
+ "unicodeVersion": "6.0",
"digest": "6925654de642e50f84661f94364a96c87757d73fffe766aacbf4bbd70130547b"
},
- {
- "name": "clock8",
- "unicode": "1F557",
+ "clock8": {
+ "category": "symbols",
+ "moji": "🕗",
+ "unicodeVersion": "6.0",
"digest": "9be2d189c7ea56d39fd259f84853d753c1cf33e64f8ed57f86f822d9ae23a1ee"
},
- {
- "name": "clock830",
- "unicode": "1F563",
+ "clock830": {
+ "category": "symbols",
+ "moji": "🕣",
+ "unicodeVersion": "6.0",
"digest": "16878613c0000d2f558c88d080551f424a8bd9df1358e0f931dd25c3da68f2d9"
},
- {
- "name": "clock9",
- "unicode": "1F558",
+ "clock9": {
+ "category": "symbols",
+ "moji": "🕘",
+ "unicodeVersion": "6.0",
"digest": "1d1e7e3c9d085ffa5b7c0f3d9fd394b734f16ae3b60df09af50fe6c8d4f3c8bb"
},
- {
- "name": "clock930",
- "unicode": "1F564",
+ "clock930": {
+ "category": "symbols",
+ "moji": "🕤",
+ "unicodeVersion": "6.0",
"digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74"
},
- {
- "name": "closed_book",
- "unicode": "1F4D5",
+ "closed_book": {
+ "category": "objects",
+ "moji": "📕",
+ "unicodeVersion": "6.0",
"digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f"
},
- {
- "name": "closed_lock_with_key",
- "unicode": "1F510",
+ "closed_lock_with_key": {
+ "category": "objects",
+ "moji": "🔐",
+ "unicodeVersion": "6.0",
"digest": "e39adfe9b30973bca16472c2b7e6462b064a93b9d452aa48edd74c727641a83d"
},
- {
- "name": "closed_umbrella",
- "unicode": "1F302",
+ "closed_umbrella": {
+ "category": "people",
+ "moji": "🌂",
+ "unicodeVersion": "6.0",
"digest": "2cc0592c74601f7439e88c3c1ec4f05e3459608ef1ea6558c5824ed7c3889727"
},
- {
- "name": "cloud",
- "unicode": "2601",
+ "cloud": {
+ "category": "nature",
+ "moji": "☁",
+ "unicodeVersion": "1.1",
"digest": "5b3a19718dfa8a381929665afdc2284464d24020c8dd0caff4dad465a1f536ba"
},
- {
- "name": "cloud_lightning",
- "unicode": "1F329",
+ "cloud_lightning": {
+ "category": "nature",
+ "moji": "🌩",
+ "unicodeVersion": "7.0",
"digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
},
- {
- "name": "cloud_with_lightning",
- "unicode": "1F329",
- "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9"
- },
- {
- "name": "cloud_rain",
- "unicode": "1F327",
- "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
- },
- {
- "name": "cloud_with_rain",
- "unicode": "1F327",
+ "cloud_rain": {
+ "category": "nature",
+ "moji": "🌧",
+ "unicodeVersion": "7.0",
"digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71"
},
- {
- "name": "cloud_snow",
- "unicode": "1F328",
- "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
- },
- {
- "name": "cloud_with_snow",
- "unicode": "1F328",
+ "cloud_snow": {
+ "category": "nature",
+ "moji": "🌨",
+ "unicodeVersion": "7.0",
"digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1"
},
- {
- "name": "cloud_tornado",
- "unicode": "1F32A",
- "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
- },
- {
- "name": "cloud_with_tornado",
- "unicode": "1F32A",
+ "cloud_tornado": {
+ "category": "nature",
+ "moji": "🌪",
+ "unicodeVersion": "7.0",
"digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
},
- {
- "name": "clown",
- "unicode": "1F921",
- "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
- },
- {
- "name": "clown_face",
- "unicode": "1F921",
+ "clown": {
+ "category": "people",
+ "moji": "🤡",
+ "unicodeVersion": "9.0",
"digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
},
- {
- "name": "clubs",
- "unicode": "2663",
+ "clubs": {
+ "category": "symbols",
+ "moji": "♣",
+ "unicodeVersion": "1.1",
"digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138"
},
- {
- "name": "cocktail",
- "unicode": "1F378",
+ "cocktail": {
+ "category": "food",
+ "moji": "🍸",
+ "unicodeVersion": "6.0",
"digest": "3792def2cde885cf32167f04904d3b0b788388e8af410c63e4cd31550feba775"
},
- {
- "name": "coffee",
- "unicode": "2615",
+ "coffee": {
+ "category": "food",
+ "moji": "☕",
+ "unicodeVersion": "4.0",
"digest": "0d29615a7a67d3aafa257b909bb915dc74fa8f854acb0d9a2c29e94eedf80326"
},
- {
- "name": "coffin",
- "unicode": "26B0",
+ "coffin": {
+ "category": "objects",
+ "moji": "⚰",
+ "unicodeVersion": "4.1",
"digest": "78eccc1aad2a822649fba8503d4d30354bef367c4271193c40ddb692308f9db8"
},
- {
- "name": "cold_sweat",
- "unicode": "1F630",
+ "cold_sweat": {
+ "category": "people",
+ "moji": "😰",
+ "unicodeVersion": "6.0",
"digest": "f53aab523ed3fa2224a16881d263fb5e039f163380f92feb2c63c20f9b14dcd2"
},
- {
- "name": "comet",
- "unicode": "2604",
+ "comet": {
+ "category": "nature",
+ "moji": "☄",
+ "unicodeVersion": "1.1",
"digest": "40ce93e55c6e57a88d80670b37171190bd5ffc87b7078891d8de5b15795385c5"
},
- {
- "name": "compression",
- "unicode": "1F5DC",
+ "compression": {
+ "category": "objects",
+ "moji": "🗜",
+ "unicodeVersion": "7.0",
"digest": "c8841f7afb5345f1c31da116a7fb41d07232ea58d3f7f1a75c5890aa1a80bfd6"
},
- {
- "name": "computer",
- "unicode": "1F4BB",
+ "computer": {
+ "category": "objects",
+ "moji": "💻",
+ "unicodeVersion": "6.0",
"digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93"
},
- {
- "name": "confetti_ball",
- "unicode": "1F38A",
+ "confetti_ball": {
+ "category": "objects",
+ "moji": "🎊",
+ "unicodeVersion": "6.0",
"digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10"
},
- {
- "name": "confounded",
- "unicode": "1F616",
+ "confounded": {
+ "category": "people",
+ "moji": "😖",
+ "unicodeVersion": "6.0",
"digest": "e2ff3b4df65d00c1ca9ae0cb379f959ea2cecefb3d676d4f8c2c5f2c103da4f6"
},
- {
- "name": "confused",
- "unicode": "1F615",
+ "confused": {
+ "category": "people",
+ "moji": "😕",
+ "unicodeVersion": "6.1",
"digest": "118d7f830ec08a3ac4b798eebb77a989b8c142f2588727181be4a2548e3c4f06"
},
- {
- "name": "congratulations",
- "unicode": "3297",
+ "congratulations": {
+ "category": "symbols",
+ "moji": "㊗",
+ "unicodeVersion": "1.1",
"digest": "02fd1338c54fe5f9a0fd861f23c56edc1d39bcd3140b68f0f626f9e2494d2d1c"
},
- {
- "name": "construction",
- "unicode": "1F6A7",
+ "construction": {
+ "category": "travel",
+ "moji": "🚧",
+ "unicodeVersion": "6.0",
"digest": "c3a0401331111b9eda1206bee5f322db80b0870547d307b10dcac1314e4078c8"
},
- {
- "name": "construction_site",
- "unicode": "1F3D7",
- "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
- },
- {
- "name": "building_construction",
- "unicode": "1F3D7",
+ "construction_site": {
+ "category": "travel",
+ "moji": "🏗",
+ "unicodeVersion": "7.0",
"digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
},
- {
- "name": "construction_worker",
- "unicode": "1F477",
+ "construction_worker": {
+ "category": "people",
+ "moji": "👷",
+ "unicodeVersion": "6.0",
"digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6"
},
- {
- "name": "construction_worker_tone1",
- "unicode": "1F477-1F3FB",
+ "construction_worker_tone1": {
+ "category": "people",
+ "moji": "👷🏻",
+ "unicodeVersion": "8.0",
"digest": "fcd927405fef4486105cd3aff62155467d21cebbc013924d4b52b717b566602b"
},
- {
- "name": "construction_worker_tone2",
- "unicode": "1F477-1F3FC",
+ "construction_worker_tone2": {
+ "category": "people",
+ "moji": "👷🏼",
+ "unicodeVersion": "8.0",
"digest": "d1ec773828936c703dd6e334e696dc3cf7c34c0a8ec691564a384b735cdeaaba"
},
- {
- "name": "construction_worker_tone3",
- "unicode": "1F477-1F3FD",
+ "construction_worker_tone3": {
+ "category": "people",
+ "moji": "👷🏽",
+ "unicodeVersion": "8.0",
"digest": "37c114d6879b9b32b800b0d4cf770dcbe04d1455698130ecd709a0cb9dea880b"
},
- {
- "name": "construction_worker_tone4",
- "unicode": "1F477-1F3FE",
+ "construction_worker_tone4": {
+ "category": "people",
+ "moji": "👷🏾",
+ "unicodeVersion": "8.0",
"digest": "5264996c1bedb6061a0dfdddce233d863bf308d27127ad152b63bfd983162cf7"
},
- {
- "name": "construction_worker_tone5",
- "unicode": "1F477-1F3FF",
+ "construction_worker_tone5": {
+ "category": "people",
+ "moji": "👷🏿",
+ "unicodeVersion": "8.0",
"digest": "87051aec81fd5dfd4dc44ff0411a528ee08253e9494d37efa550694e28dde6d3"
},
- {
- "name": "control_knobs",
- "unicode": "1F39B",
+ "control_knobs": {
+ "category": "objects",
+ "moji": "🎛",
+ "unicodeVersion": "7.0",
"digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb"
},
- {
- "name": "convenience_store",
- "unicode": "1F3EA",
+ "convenience_store": {
+ "category": "travel",
+ "moji": "🏪",
+ "unicodeVersion": "6.0",
"digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52"
},
- {
- "name": "cookie",
- "unicode": "1F36A",
+ "cookie": {
+ "category": "food",
+ "moji": "🍪",
+ "unicodeVersion": "6.0",
"digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4"
},
- {
- "name": "cooking",
- "unicode": "1F373",
+ "cooking": {
+ "category": "food",
+ "moji": "🍳",
+ "unicodeVersion": "6.0",
"digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58"
},
- {
- "name": "cool",
- "unicode": "1F192",
+ "cool": {
+ "category": "symbols",
+ "moji": "🆒",
+ "unicodeVersion": "6.0",
"digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1"
},
- {
- "name": "cop",
- "unicode": "1F46E",
+ "cop": {
+ "category": "people",
+ "moji": "👮",
+ "unicodeVersion": "6.0",
"digest": "78996521bbe231d03ebea355226d8a1515f47cde7b2fbeca1037e7b7e5133466"
},
- {
- "name": "cop_tone1",
- "unicode": "1F46E-1F3FB",
+ "cop_tone1": {
+ "category": "people",
+ "moji": "👮🏻",
+ "unicodeVersion": "8.0",
"digest": "8a38cd107f5f4c0b821ac43f32df5dc57facaf39fbafb98483ec00fd7df41baf"
},
- {
- "name": "cop_tone2",
- "unicode": "1F46E-1F3FC",
+ "cop_tone2": {
+ "category": "people",
+ "moji": "👮🏼",
+ "unicodeVersion": "8.0",
"digest": "8ab8ab086f3ff82aa4bf4760c3c822846ec2696c41d21dffdac12d5afbe398b7"
},
- {
- "name": "cop_tone3",
- "unicode": "1F46E-1F3FD",
+ "cop_tone3": {
+ "category": "people",
+ "moji": "👮🏽",
+ "unicodeVersion": "8.0",
"digest": "fce710a99fd44a7c8af3ea01b2007e46d3ff38d7a0dff1ef26d6f893ede7e6d2"
},
- {
- "name": "cop_tone4",
- "unicode": "1F46E-1F3FE",
+ "cop_tone4": {
+ "category": "people",
+ "moji": "👮🏾",
+ "unicodeVersion": "8.0",
"digest": "3017dd73ef475379911c5e6c79bd0f9f533dbbc5057bce6a11244faa12996ba0"
},
- {
- "name": "cop_tone5",
- "unicode": "1F46E-1F3FF",
+ "cop_tone5": {
+ "category": "people",
+ "moji": "👮🏿",
+ "unicodeVersion": "8.0",
"digest": "a3b8807b3f2a8d6ee9bcec0339355bda486e8c930f727139f5447a4b046a6307"
},
- {
- "name": "copyright",
- "unicode": "00A9",
+ "copyright": {
+ "category": "symbols",
+ "moji": "©",
+ "unicodeVersion": "1.1",
"digest": "cc28663cdd3f8333d9bb57b511348cde4e51bda19cf0629dccb05c8fc425e079"
},
- {
- "name": "corn",
- "unicode": "1F33D",
+ "corn": {
+ "category": "food",
+ "moji": "🌽",
+ "unicodeVersion": "6.0",
"digest": "a099a0b291fa758690e6ee6c762b9ade9a0e3350a707c52d968dfffbcc467de5"
},
- {
- "name": "couch",
- "unicode": "1F6CB",
- "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
- },
- {
- "name": "couch_and_lamp",
- "unicode": "1F6CB",
+ "couch": {
+ "category": "objects",
+ "moji": "🛋",
+ "unicodeVersion": "7.0",
"digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474"
},
- {
- "name": "couple",
- "unicode": "1F46B",
+ "couple": {
+ "category": "people",
+ "moji": "👫",
+ "unicodeVersion": "6.0",
"digest": "c897ba76e24e2f43a4aa261c2754800a8473f43c7ce53f9909a6af2c4897732a"
},
- {
- "name": "couple_mm",
- "unicode": "1F468-2764-1F468",
+ "couple_mm": {
+ "category": "people",
+ "moji": "👨‍❤️‍👨",
+ "unicodeVersion": "6.0",
"digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
},
- {
- "name": "couple_with_heart_mm",
- "unicode": "1F468-2764-1F468",
- "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803"
- },
- {
- "name": "couple_with_heart",
- "unicode": "1F491",
+ "couple_with_heart": {
+ "category": "people",
+ "moji": "💑",
+ "unicodeVersion": "6.0",
"digest": "420bfa81bad10365550c77a98e1c07eb00d03663fe7b610fab1aca8a0a9d201b"
},
- {
- "name": "couple_ww",
- "unicode": "1F469-2764-1F469",
- "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
- },
- {
- "name": "couple_with_heart_ww",
- "unicode": "1F469-2764-1F469",
+ "couple_ww": {
+ "category": "people",
+ "moji": "👩‍❤️‍👩",
+ "unicodeVersion": "6.0",
"digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e"
},
- {
- "name": "couplekiss",
- "unicode": "1F48F",
+ "couplekiss": {
+ "category": "people",
+ "moji": "💏",
+ "unicodeVersion": "6.0",
"digest": "1acfef9d375c4c1deb235babd856b0f90ad4f3194751694cb6abb44f00f29e42"
},
- {
- "name": "cow",
- "unicode": "1F42E",
+ "cow": {
+ "category": "nature",
+ "moji": "🐮",
+ "unicodeVersion": "6.0",
"digest": "d71c854ff8b343ee24b8c2b9d56c7cb3fc6fa1a6dc0d7a137841b9f646e6d71b"
},
- {
- "name": "cow2",
- "unicode": "1F404",
+ "cow2": {
+ "category": "nature",
+ "moji": "🐄",
+ "unicodeVersion": "6.0",
"digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339"
},
- {
- "name": "cowboy",
- "unicode": "1F920",
+ "cowboy": {
+ "category": "people",
+ "moji": "🤠",
+ "unicodeVersion": "9.0",
"digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
},
- {
- "name": "face_with_cowboy_hat",
- "unicode": "1F920",
- "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
- },
- {
- "name": "crab",
- "unicode": "1F980",
+ "crab": {
+ "category": "nature",
+ "moji": "🦀",
+ "unicodeVersion": "8.0",
"digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5"
},
- {
- "name": "crayon",
- "unicode": "1F58D",
- "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
- },
- {
- "name": "lower_left_crayon",
- "unicode": "1F58D",
+ "crayon": {
+ "category": "objects",
+ "moji": "🖍",
+ "unicodeVersion": "7.0",
"digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a"
},
- {
- "name": "credit_card",
- "unicode": "1F4B3",
+ "credit_card": {
+ "category": "objects",
+ "moji": "💳",
+ "unicodeVersion": "6.0",
"digest": "808cd120fd3738eb2be1f6c6c029d98387b0e03fca7d1451e8fbf9c5ab3f643f"
},
- {
- "name": "crescent_moon",
- "unicode": "1F319",
+ "crescent_moon": {
+ "category": "nature",
+ "moji": "🌙",
+ "unicodeVersion": "6.0",
"digest": "042e7e01e6e88b97a763b7cc41e2a2b3fe68a649bacf4a090cd28fc653baf640"
},
- {
- "name": "cricket",
- "unicode": "1F3CF",
+ "cricket": {
+ "category": "activity",
+ "moji": "🏏",
+ "unicodeVersion": "8.0",
"digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
},
- {
- "name": "cricket_bat_ball",
- "unicode": "1F3CF",
- "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16"
- },
- {
- "name": "crocodile",
- "unicode": "1F40A",
+ "crocodile": {
+ "category": "nature",
+ "moji": "🐊",
+ "unicodeVersion": "6.0",
"digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992"
},
- {
- "name": "croissant",
- "unicode": "1F950",
+ "croissant": {
+ "category": "food",
+ "moji": "🥐",
+ "unicodeVersion": "9.0",
"digest": "b751e287157a1e276617a841a5b5f7f1208ca226cfd8fa947f144390b65a5e16"
},
- {
- "name": "cross",
- "unicode": "271D",
- "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
- },
- {
- "name": "latin_cross",
- "unicode": "271D",
+ "cross": {
+ "category": "symbols",
+ "moji": "✝",
+ "unicodeVersion": "1.1",
"digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
},
- {
- "name": "crossed_flags",
- "unicode": "1F38C",
+ "crossed_flags": {
+ "category": "objects",
+ "moji": "🎌",
+ "unicodeVersion": "6.0",
"digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262"
},
- {
- "name": "crossed_swords",
- "unicode": "2694",
+ "crossed_swords": {
+ "category": "objects",
+ "moji": "⚔",
+ "unicodeVersion": "4.1",
"digest": "3771a5b26b514236521ce44e15f7730fa9148c6a782b9b600ab870a1f7de6f9f"
},
- {
- "name": "crown",
- "unicode": "1F451",
+ "crown": {
+ "category": "people",
+ "moji": "👑",
+ "unicodeVersion": "6.0",
"digest": "6741e58d8f823194e0a3484ac1563e20d9e0b44c1bc46d82444dfffa092cdfc7"
},
- {
- "name": "cruise_ship",
- "unicode": "1F6F3",
+ "cruise_ship": {
+ "category": "travel",
+ "moji": "🛳",
+ "unicodeVersion": "7.0",
"digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
},
- {
- "name": "passenger_ship",
- "unicode": "1F6F3",
- "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4"
- },
- {
- "name": "cry",
- "unicode": "1F622",
+ "cry": {
+ "category": "people",
+ "moji": "😢",
+ "unicodeVersion": "6.0",
"digest": "fc3307ec4fe75539770c1123a0e8e721d9e021009a502655132f68d7cc453816"
},
- {
- "name": "crying_cat_face",
- "unicode": "1F63F",
+ "crying_cat_face": {
+ "category": "people",
+ "moji": "😿",
+ "unicodeVersion": "6.0",
"digest": "4942c24935c22babdcb8af41d2c0a7588356b6b674bc238902e2f10ad03e2c5b"
},
- {
- "name": "crystal_ball",
- "unicode": "1F52E",
+ "crystal_ball": {
+ "category": "objects",
+ "moji": "🔮",
+ "unicodeVersion": "6.0",
"digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30"
},
- {
- "name": "cucumber",
- "unicode": "1F952",
+ "cucumber": {
+ "category": "food",
+ "moji": "🥒",
+ "unicodeVersion": "9.0",
"digest": "d1196e23f2f155ef5c1330f8497f40957a7357cb177127f457c5c471f0a23727"
},
- {
- "name": "cupid",
- "unicode": "1F498",
+ "cupid": {
+ "category": "symbols",
+ "moji": "💘",
+ "unicodeVersion": "6.0",
"digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658"
},
- {
- "name": "curly_loop",
- "unicode": "27B0",
+ "curly_loop": {
+ "category": "symbols",
+ "moji": "➰",
+ "unicodeVersion": "6.0",
"digest": "9e4eb98d6597888f91208080c6a79824adb432ea34f46c85da26cb630bd1cc73"
},
- {
- "name": "currency_exchange",
- "unicode": "1F4B1",
+ "currency_exchange": {
+ "category": "symbols",
+ "moji": "💱",
+ "unicodeVersion": "6.0",
"digest": "b85377265b9876888969aa42b65bba0be523a370175baf226f20131e535af554"
},
- {
- "name": "curry",
- "unicode": "1F35B",
+ "curry": {
+ "category": "food",
+ "moji": "🍛",
+ "unicodeVersion": "6.0",
"digest": "a01c0a713662817720b485f7739f57e61afc025f5c43792f4de961c94f92f31e"
},
- {
- "name": "custard",
- "unicode": "1F36E",
+ "custard": {
+ "category": "food",
+ "moji": "🍮",
+ "unicodeVersion": "6.0",
"digest": "85c2b9ac904134a6c3587eb0a0806f2ab4282c5ed5c79d41734f3203998f757e"
},
- {
- "name": "customs",
- "unicode": "1F6C3",
+ "customs": {
+ "category": "symbols",
+ "moji": "🛃",
+ "unicodeVersion": "6.0",
"digest": "eb2546e1e617d4c1a1f614318af5e5dacf3e8d9479ffa08108977defa83ded32"
},
- {
- "name": "cyclone",
- "unicode": "1F300",
+ "cyclone": {
+ "category": "symbols",
+ "moji": "🌀",
+ "unicodeVersion": "6.0",
"digest": "7a0f8564d76adf2d0ed272f56dc0d01fb7b557852e0ca797e73f5472b8630bf3"
},
- {
- "name": "dagger",
- "unicode": "1F5E1",
+ "dagger": {
+ "category": "objects",
+ "moji": "🗡",
+ "unicodeVersion": "7.0",
"digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
},
- {
- "name": "dagger_knife",
- "unicode": "1F5E1",
- "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772"
- },
- {
- "name": "dancer",
- "unicode": "1F483",
+ "dancer": {
+ "category": "people",
+ "moji": "💃",
+ "unicodeVersion": "6.0",
"digest": "66ffa86827e85acae4aa870c0859fe3a9dad03d21ff4bc800b61c95c902a8a90"
},
- {
- "name": "dancer_tone1",
- "unicode": "1F483-1F3FB",
+ "dancer_tone1": {
+ "category": "people",
+ "moji": "💃🏻",
+ "unicodeVersion": "8.0",
"digest": "bdbee740addc890e369d3469a3585eb0d1e4fbc7e04dd6f6aca762d8aeee6a8c"
},
- {
- "name": "dancer_tone2",
- "unicode": "1F483-1F3FC",
+ "dancer_tone2": {
+ "category": "people",
+ "moji": "💃🏼",
+ "unicodeVersion": "8.0",
"digest": "9f7b4c627241eaa2def9717a5286a423f0b9c1b044dd9ea4442a76f1858d14a4"
},
- {
- "name": "dancer_tone3",
- "unicode": "1F483-1F3FD",
+ "dancer_tone3": {
+ "category": "people",
+ "moji": "💃🏽",
+ "unicodeVersion": "8.0",
"digest": "a6bd49a377ce6c2004bf126b6f66d0b21d8c14103c2add7b10f12ed9e1c2d302"
},
- {
- "name": "dancer_tone4",
- "unicode": "1F483-1F3FE",
+ "dancer_tone4": {
+ "category": "people",
+ "moji": "💃🏾",
+ "unicodeVersion": "8.0",
"digest": "4ec2a7629c01b0e9006b5cda4deae3bf297ce3b71d18063f93eeb5c14be19a1a"
},
- {
- "name": "dancer_tone5",
- "unicode": "1F483-1F3FF",
+ "dancer_tone5": {
+ "category": "people",
+ "moji": "💃🏿",
+ "unicodeVersion": "8.0",
"digest": "2b48e3a6b366c6f55f73b816e6fb03c39e9890f586f7e9c9043cf0c013d9cdd5"
},
- {
- "name": "dancers",
- "unicode": "1F46F",
+ "dancers": {
+ "category": "people",
+ "moji": "👯",
+ "unicodeVersion": "6.0",
"digest": "12be66ed19d232bb387270f40bece68bd0cb2342b318f6c9bb8b49c64ff7d0ad"
},
- {
- "name": "dango",
- "unicode": "1F361",
+ "dango": {
+ "category": "food",
+ "moji": "🍡",
+ "unicodeVersion": "6.0",
"digest": "34e8cd153c50f2d725abe8934c35c96a3ab533f0cc5fbb1e1474eafad1dc1fc2"
},
- {
- "name": "dark_sunglasses",
- "unicode": "1F576",
+ "dark_sunglasses": {
+ "category": "people",
+ "moji": "🕶",
+ "unicodeVersion": "7.0",
"digest": "d0a735ad5bf0ece00af2a21abf950b89292ebd8ca6e28b1dbb1368252fb44afe"
},
- {
- "name": "dart",
- "unicode": "1F3AF",
+ "dart": {
+ "category": "activity",
+ "moji": "🎯",
+ "unicodeVersion": "6.0",
"digest": "998642f06a875905e0a6bf30963c025baff1cf55b8e76884b9119f2d71188b0c"
},
- {
- "name": "dash",
- "unicode": "1F4A8",
+ "dash": {
+ "category": "nature",
+ "moji": "💨",
+ "unicodeVersion": "6.0",
"digest": "f7aae7d3887c67d76f3329c2dc9e6807dc580a4b07ab35599c7805e41823a345"
},
- {
- "name": "date",
- "unicode": "1F4C5",
+ "date": {
+ "category": "objects",
+ "moji": "📅",
+ "unicodeVersion": "6.0",
"digest": "d0b695e4a7cfbbe71b4fbebf345b66ca98f0cf1c751362928e54c23ca78d4c7b"
},
- {
- "name": "deciduous_tree",
- "unicode": "1F333",
+ "deciduous_tree": {
+ "category": "nature",
+ "moji": "🌳",
+ "unicodeVersion": "6.0",
"digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790"
},
- {
- "name": "deer",
- "unicode": "1F98C",
+ "deer": {
+ "category": "nature",
+ "moji": "🦌",
+ "unicodeVersion": "9.0",
"digest": "7f4302ca68fd121ee73be48d0a0a0fb9e7e2741071a491ad2b7b0eab9f11ad25"
},
- {
- "name": "department_store",
- "unicode": "1F3EC",
+ "department_store": {
+ "category": "travel",
+ "moji": "🏬",
+ "unicodeVersion": "6.0",
"digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b"
},
- {
- "name": "desert",
- "unicode": "1F3DC",
+ "desert": {
+ "category": "travel",
+ "moji": "🏜",
+ "unicodeVersion": "7.0",
"digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e"
},
- {
- "name": "desktop",
- "unicode": "1F5A5",
+ "desktop": {
+ "category": "objects",
+ "moji": "🖥",
+ "unicodeVersion": "7.0",
"digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
},
- {
- "name": "desktop_computer",
- "unicode": "1F5A5",
- "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
- },
- {
- "name": "diamond_shape_with_a_dot_inside",
- "unicode": "1F4A0",
+ "diamond_shape_with_a_dot_inside": {
+ "category": "symbols",
+ "moji": "💠",
+ "unicodeVersion": "6.0",
"digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3"
},
- {
- "name": "diamonds",
- "unicode": "2666",
+ "diamonds": {
+ "category": "symbols",
+ "moji": "♦",
+ "unicodeVersion": "1.1",
"digest": "bf3d9a020afe8aa226db73590bc193a9c2c3e6e642edd2445c5960c3e67cf153"
},
- {
- "name": "disappointed",
- "unicode": "1F61E",
+ "disappointed": {
+ "category": "people",
+ "moji": "😞",
+ "unicodeVersion": "6.0",
"digest": "c0f406c6beea0fd1328adefc097d04aa16b72f7a5afa0867967d8ea25d72db17"
},
- {
- "name": "disappointed_relieved",
- "unicode": "1F625",
+ "disappointed_relieved": {
+ "category": "people",
+ "moji": "😥",
+ "unicodeVersion": "6.0",
"digest": "c826f5dd4f2f7e5289d720851d4826ab8284d915606c1b152ab229b7fadbba14"
},
- {
- "name": "dividers",
- "unicode": "1F5C2",
- "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
- },
- {
- "name": "card_index_dividers",
- "unicode": "1F5C2",
+ "dividers": {
+ "category": "objects",
+ "moji": "🗂",
+ "unicodeVersion": "7.0",
"digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f"
},
- {
- "name": "dizzy",
- "unicode": "1F4AB",
+ "dizzy": {
+ "category": "nature",
+ "moji": "💫",
+ "unicodeVersion": "6.0",
"digest": "d577545c2de42389695447c6ebbfef895f30f0fda84eef45684f9bf4a9c27ff1"
},
- {
- "name": "dizzy_face",
- "unicode": "1F635",
+ "dizzy_face": {
+ "category": "people",
+ "moji": "😵",
+ "unicodeVersion": "6.0",
"digest": "7b3aeaffb4e15ccf633b91dda4a44847a1eb28d78ce58b4d171b20a771bde414"
},
- {
- "name": "do_not_litter",
- "unicode": "1F6AF",
+ "do_not_litter": {
+ "category": "symbols",
+ "moji": "🚯",
+ "unicodeVersion": "6.0",
"digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb"
},
- {
- "name": "dog",
- "unicode": "1F436",
+ "dog": {
+ "category": "nature",
+ "moji": "🐶",
+ "unicodeVersion": "6.0",
"digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11"
},
- {
- "name": "dog2",
- "unicode": "1F415",
+ "dog2": {
+ "category": "nature",
+ "moji": "🐕",
+ "unicodeVersion": "6.0",
"digest": "0a8901bce5ed994533ff84299b2a1364de28d872c9f9510d3426a83e8a9d2e34"
},
- {
- "name": "dollar",
- "unicode": "1F4B5",
+ "dollar": {
+ "category": "objects",
+ "moji": "💵",
+ "unicodeVersion": "6.0",
"digest": "52438e38867aedc021740bb41f9ba336e75a50faa148419412a01d75d8c93155"
},
- {
- "name": "dolls",
- "unicode": "1F38E",
+ "dolls": {
+ "category": "objects",
+ "moji": "🎎",
+ "unicodeVersion": "6.0",
"digest": "a687184e9a0915deef44bb3cacfb19d3f3f19cf2c110f1da90191dd567333c57"
},
- {
- "name": "dolphin",
- "unicode": "1F42C",
+ "dolphin": {
+ "category": "nature",
+ "moji": "🐬",
+ "unicodeVersion": "6.0",
"digest": "0b7ee08f4236232ca533ed3a3023d28020d36f178efaec5ce8b0e13a84778512"
},
- {
- "name": "door",
- "unicode": "1F6AA",
+ "door": {
+ "category": "objects",
+ "moji": "🚪",
+ "unicodeVersion": "6.0",
"digest": "984a9ca88852ebdb539e0c385d9c6ffe5010e9189bc372a3d00f5c8d44c8e6f5"
},
- {
- "name": "doughnut",
- "unicode": "1F369",
+ "doughnut": {
+ "category": "food",
+ "moji": "🍩",
+ "unicodeVersion": "6.0",
"digest": "27634587e6a53807baa32157bb06b0e115c8ad8aefebba7ebb0b65a084170e3a"
},
- {
- "name": "dove",
- "unicode": "1F54A",
+ "dove": {
+ "category": "nature",
+ "moji": "🕊",
+ "unicodeVersion": "7.0",
"digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
},
- {
- "name": "dove_of_peace",
- "unicode": "1F54A",
- "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3"
- },
- {
- "name": "dragon",
- "unicode": "1F409",
+ "dragon": {
+ "category": "nature",
+ "moji": "🐉",
+ "unicodeVersion": "6.0",
"digest": "2abcb3d945d848e34ffc76203b29ef26df7458856166fffd155611f7bbe72652"
},
- {
- "name": "dragon_face",
- "unicode": "1F432",
+ "dragon_face": {
+ "category": "nature",
+ "moji": "🐲",
+ "unicodeVersion": "6.0",
"digest": "0030548931b931e3b51f26cf660394aee36499e688ba83ce9cfccb635dcd4d54"
},
- {
- "name": "dress",
- "unicode": "1F457",
+ "dress": {
+ "category": "people",
+ "moji": "👗",
+ "unicodeVersion": "6.0",
"digest": "96ceba928fb356f7c0ae99bf22552321f08a65d5f1c0340ab89641219ad366ad"
},
- {
- "name": "dromedary_camel",
- "unicode": "1F42A",
+ "dromedary_camel": {
+ "category": "nature",
+ "moji": "🐪",
+ "unicodeVersion": "6.0",
"digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d"
},
- {
- "name": "drooling_face",
- "unicode": "1F924",
- "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
- },
- {
- "name": "drool",
- "unicode": "1F924",
+ "drooling_face": {
+ "category": "people",
+ "moji": "🤤",
+ "unicodeVersion": "9.0",
"digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
},
- {
- "name": "droplet",
- "unicode": "1F4A7",
+ "droplet": {
+ "category": "nature",
+ "moji": "💧",
+ "unicodeVersion": "6.0",
"digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3"
},
- {
- "name": "drum",
- "unicode": "1F941",
- "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
- },
- {
- "name": "drum_with_drumsticks",
- "unicode": "1F941",
+ "drum": {
+ "category": "activity",
+ "moji": "🥁",
+ "unicodeVersion": "9.0",
"digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
},
- {
- "name": "duck",
- "unicode": "1F986",
+ "duck": {
+ "category": "nature",
+ "moji": "🦆",
+ "unicodeVersion": "9.0",
"digest": "8f8373798a7727368b32328e7a9a349727a949e7391ddd243b6456141a4f7e94"
},
- {
- "name": "dvd",
- "unicode": "1F4C0",
+ "dvd": {
+ "category": "objects",
+ "moji": "📀",
+ "unicodeVersion": "6.0",
"digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f"
},
- {
- "name": "e-mail",
- "unicode": "1F4E7",
- "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
- },
- {
- "name": "email",
- "unicode": "1F4E7",
+ "e-mail": {
+ "category": "objects",
+ "moji": "📧",
+ "unicodeVersion": "6.0",
"digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
},
- {
- "name": "eagle",
- "unicode": "1F985",
+ "eagle": {
+ "category": "nature",
+ "moji": "🦅",
+ "unicodeVersion": "9.0",
"digest": "b44fd4f61b83c5114358a272343ac9b0eabbc70847f739bbdbf8aae3ade5bc1d"
},
- {
- "name": "ear",
- "unicode": "1F442",
+ "ear": {
+ "category": "people",
+ "moji": "👂",
+ "unicodeVersion": "6.0",
"digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8"
},
- {
- "name": "ear_of_rice",
- "unicode": "1F33E",
+ "ear_of_rice": {
+ "category": "nature",
+ "moji": "🌾",
+ "unicodeVersion": "6.0",
"digest": "2997c340c2b333d6ba9b73f94ff1a1881735fe0cc4f0c72d7719b305499fc425"
},
- {
- "name": "ear_tone1",
- "unicode": "1F442-1F3FB",
+ "ear_tone1": {
+ "category": "people",
+ "moji": "👂🏻",
+ "unicodeVersion": "8.0",
"digest": "5ca759b8569a377a4e63e30d94b585b9f76d15348a8a0c1ba19fdc522790615e"
},
- {
- "name": "ear_tone2",
- "unicode": "1F442-1F3FC",
+ "ear_tone2": {
+ "category": "people",
+ "moji": "👂🏼",
+ "unicodeVersion": "8.0",
"digest": "12aafb3ef2cfcdc892b2877c2e24920620f0f77f850e12afbfe55eadce9e37df"
},
- {
- "name": "ear_tone3",
- "unicode": "1F442-1F3FD",
+ "ear_tone3": {
+ "category": "people",
+ "moji": "👂🏽",
+ "unicodeVersion": "8.0",
"digest": "f4d28d9f72cf116ac92d80061eb84c918d6523bf53b2ad526f5457aba487d527"
},
- {
- "name": "ear_tone4",
- "unicode": "1F442-1F3FE",
+ "ear_tone4": {
+ "category": "people",
+ "moji": "👂🏾",
+ "unicodeVersion": "8.0",
"digest": "eaa9453670f7e3adc6ec6934ee70efc9bf60fe6c99c5804b7ba9e3804aec65de"
},
- {
- "name": "ear_tone5",
- "unicode": "1F442-1F3FF",
+ "ear_tone5": {
+ "category": "people",
+ "moji": "👂🏿",
+ "unicodeVersion": "8.0",
"digest": "54bd0782419489556b80e9e0d15b05df74757aa4e04ba565f45c20d3dd60e3f1"
},
- {
- "name": "earth_africa",
- "unicode": "1F30D",
+ "earth_africa": {
+ "category": "nature",
+ "moji": "🌍",
+ "unicodeVersion": "6.0",
"digest": "c691a6f591f5a07b268fd64efe113e81cec8d5963ad83ced2537422343ff7ecf"
},
- {
- "name": "earth_americas",
- "unicode": "1F30E",
+ "earth_americas": {
+ "category": "nature",
+ "moji": "🌎",
+ "unicodeVersion": "6.0",
"digest": "a9c60cf8341ff59a9cc1a715b7144af734fcd28915a8e003a31ebf2abf9aedb1"
},
- {
- "name": "earth_asia",
- "unicode": "1F30F",
+ "earth_asia": {
+ "category": "nature",
+ "moji": "🌏",
+ "unicodeVersion": "6.0",
"digest": "ee2beb61fb8c87279161c5a8c4ad17bb71ce790123f8fa33522941d027e060a5"
},
- {
- "name": "egg",
- "unicode": "1F95A",
+ "egg": {
+ "category": "food",
+ "moji": "🥚",
+ "unicodeVersion": "9.0",
"digest": "72b9c841af784e7cbccbbe48ba833df5cecdd284397c199cab079872e879d92f"
},
- {
- "name": "eggplant",
- "unicode": "1F346",
+ "eggplant": {
+ "category": "food",
+ "moji": "🍆",
+ "unicodeVersion": "6.0",
"digest": "ec0a460e0cf0e615f51279677594a899672e1b4ecd9396e17a8cfa2a3efe5238"
},
- {
- "name": "eight",
- "unicode": "0038-20E3",
+ "eight": {
+ "category": "symbols",
+ "moji": "8️⃣",
+ "unicodeVersion": "3.0",
"digest": "57ff905033a32747690adba6486d12b09eb4d45de556f4e1ab6fb04e1fb861a8"
},
- {
- "name": "eight_pointed_black_star",
- "unicode": "2734",
+ "eight_pointed_black_star": {
+ "category": "symbols",
+ "moji": "✴",
+ "unicodeVersion": "1.1",
"digest": "7bf11f6e28591e3d0625296aaabf4ecb75c982e425abf3049339e93494acc17e"
},
- {
- "name": "eight_spoked_asterisk",
- "unicode": "2733",
+ "eight_spoked_asterisk": {
+ "category": "symbols",
+ "moji": "✳",
+ "unicodeVersion": "1.1",
"digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26"
},
- {
- "name": "eject",
- "unicode": "23CF",
+ "eject": {
+ "category": "symbols",
+ "moji": "⏏",
+ "unicodeVersion": "4.0",
"digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
},
- {
- "name": "eject_symbol",
- "unicode": "23CF",
- "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
- },
- {
- "name": "electric_plug",
- "unicode": "1F50C",
+ "electric_plug": {
+ "category": "objects",
+ "moji": "🔌",
+ "unicodeVersion": "6.0",
"digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0"
},
- {
- "name": "elephant",
- "unicode": "1F418",
+ "elephant": {
+ "category": "nature",
+ "moji": "🐘",
+ "unicodeVersion": "6.0",
"digest": "b7750f4b013fbd28ac5330e1694ef4d3b4a9c6fc7b807879db0c24b035a16c29"
},
- {
- "name": "end",
- "unicode": "1F51A",
+ "end": {
+ "category": "symbols",
+ "moji": "🔚",
+ "unicodeVersion": "6.0",
"digest": "dd93aee6986eb637a8b58f234da47568b88525599f73246e322af030351997a2"
},
- {
- "name": "envelope",
- "unicode": "2709",
+ "envelope": {
+ "category": "objects",
+ "moji": "✉",
+ "unicodeVersion": "1.1",
"digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78"
},
- {
- "name": "envelope_with_arrow",
- "unicode": "1F4E9",
+ "envelope_with_arrow": {
+ "category": "objects",
+ "moji": "📩",
+ "unicodeVersion": "6.0",
"digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6"
},
- {
- "name": "euro",
- "unicode": "1F4B6",
+ "euro": {
+ "category": "objects",
+ "moji": "💶",
+ "unicodeVersion": "6.0",
"digest": "3af3e223e8f26468a94f6f5c17198432656e8d20b3bab31566c2b5a86e717df4"
},
- {
- "name": "european_castle",
- "unicode": "1F3F0",
+ "european_castle": {
+ "category": "travel",
+ "moji": "🏰",
+ "unicodeVersion": "6.0",
"digest": "21082d0be7e3b2794e59ff0170da0cfe42a9b734cf02704603e3b52ff48202ba"
},
- {
- "name": "european_post_office",
- "unicode": "1F3E4",
+ "european_post_office": {
+ "category": "travel",
+ "moji": "🏤",
+ "unicodeVersion": "6.0",
"digest": "02b4c7602939f0cb9cb2b4e05996bcdb6bd93cf8025c2ea02db8cbe13ca397d0"
},
- {
- "name": "evergreen_tree",
- "unicode": "1F332",
+ "evergreen_tree": {
+ "category": "nature",
+ "moji": "🌲",
+ "unicodeVersion": "6.0",
"digest": "74b226098e66c0a94a92e0f22b9d631736e12dca72c34182c9d0ba56aa593172"
},
- {
- "name": "exclamation",
- "unicode": "2757",
+ "exclamation": {
+ "category": "symbols",
+ "moji": "❗",
+ "unicodeVersion": "5.2",
"digest": "45b87ae4593656d7da49ff5645fb6a2a18d582553295358da9f09f1ae8272445"
},
- {
- "name": "expressionless",
- "unicode": "1F611",
+ "expressionless": {
+ "category": "people",
+ "moji": "😑",
+ "unicodeVersion": "6.1",
"digest": "34e2a1c8121f4f0bc4ce33d226d8cc1a4ebf5260746df2b23e29eef24ee9372e"
},
- {
- "name": "eye",
- "unicode": "1F441",
+ "eye": {
+ "category": "people",
+ "moji": "👁",
+ "unicodeVersion": "7.0",
"digest": "79ecff79c2edee630e72725b54e67ee2e96d24ca03fef2954a56a09c0a2227f8"
},
- {
- "name": "eye_in_speech_bubble",
- "unicode": "1F441-1F5E8",
+ "eye_in_speech_bubble": {
+ "category": "symbols",
+ "moji": "👁‍🗨",
+ "unicodeVersion": "7.0",
"digest": "c0050c026c2a3060723cab2df2603c1c7da7ed81faedb9ebe16cd89721928a55"
},
- {
- "name": "eyeglasses",
- "unicode": "1F453",
+ "eyeglasses": {
+ "category": "people",
+ "moji": "👓",
+ "unicodeVersion": "6.0",
"digest": "d4a9585d6c43ef514a97c45c64607162e775a45544821f1470c6f8f25b93ab81"
},
- {
- "name": "eyes",
- "unicode": "1F440",
+ "eyes": {
+ "category": "people",
+ "moji": "👀",
+ "unicodeVersion": "6.0",
"digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8"
},
- {
- "name": "face_palm",
- "unicode": "1F926",
+ "face_palm": {
+ "category": "people",
+ "moji": "🤦",
+ "unicodeVersion": "9.0",
"digest": "4ec873048b34b1bb34430724cf28e4bee6c0a9eee88ce39b9d1565047dc92420"
},
- {
- "name": "face_palm_tone1",
- "unicode": "1F926-1F3FB",
+ "face_palm_tone1": {
+ "category": "people",
+ "moji": "🤦🏻",
+ "unicodeVersion": "9.0",
"digest": "e93ef92b4c01dbea6c400e708e23dd36da92ccfbf5eb4f177b3b20c3a46bdc19"
},
- {
- "name": "face_palm_tone2",
- "unicode": "1F926-1F3FC",
+ "face_palm_tone2": {
+ "category": "people",
+ "moji": "🤦🏼",
+ "unicodeVersion": "9.0",
"digest": "22c8bf9fd9fa2ed9dca7a6397ed00ba6cfe9aeef2b0fb7b516ee4dda0df050ea"
},
- {
- "name": "face_palm_tone3",
- "unicode": "1F926-1F3FD",
+ "face_palm_tone3": {
+ "category": "people",
+ "moji": "🤦🏽",
+ "unicodeVersion": "9.0",
"digest": "c0b8bb9d2423e6787b6bdf1ca5a13f52853e4f48a9a1af0f2d4af1364fff022e"
},
- {
- "name": "face_palm_tone4",
- "unicode": "1F926-1F3FE",
+ "face_palm_tone4": {
+ "category": "people",
+ "moji": "🤦🏾",
+ "unicodeVersion": "9.0",
"digest": "f522ab186adcbb4549ea2c03500cdd7a86add548e43ebf7a54d58cc24deea072"
},
- {
- "name": "face_palm_tone5",
- "unicode": "1F926-1F3FF",
+ "face_palm_tone5": {
+ "category": "people",
+ "moji": "🤦🏿",
+ "unicodeVersion": "9.0",
"digest": "363507ae7178b5ec583635f47bcab10c897346f48b85d8759b1004c32cd8ad65"
},
- {
- "name": "factory",
- "unicode": "1F3ED",
+ "factory": {
+ "category": "travel",
+ "moji": "🏭",
+ "unicodeVersion": "6.0",
"digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0"
},
- {
- "name": "fallen_leaf",
- "unicode": "1F342",
+ "fallen_leaf": {
+ "category": "nature",
+ "moji": "🍂",
+ "unicodeVersion": "6.0",
"digest": "81fce04231d48db0e55f3697f930e9a7e3306bed5e35f1234e98c40a24ac5626"
},
- {
- "name": "family",
- "unicode": "1F46A",
+ "family": {
+ "category": "people",
+ "moji": "👪",
+ "unicodeVersion": "6.0",
"digest": "06f2ce63768ffe43b3d9b2a9660b34d043f37b3c91610dd62343ba21df8ecbe5"
},
- {
- "name": "family_mmb",
- "unicode": "1F468-1F468-1F466",
+ "family_mmb": {
+ "category": "people",
+ "moji": "👨‍👨‍👦",
+ "unicodeVersion": "6.0",
"digest": "41a18405be796699a7eb7c36ab6f7d898e322749997f45387377acf5bb16a50f"
},
- {
- "name": "family_mmbb",
- "unicode": "1F468-1F468-1F466-1F466",
+ "family_mmbb": {
+ "category": "people",
+ "moji": "👨‍👨‍👦‍👦",
+ "unicodeVersion": "6.0",
"digest": "87255d1d18c6971c8c083c818e598424c1bd717eed892478b7e9516639dbfb45"
},
- {
- "name": "family_mmg",
- "unicode": "1F468-1F468-1F467",
+ "family_mmg": {
+ "category": "people",
+ "moji": "👨‍👨‍👧",
+ "unicodeVersion": "6.0",
"digest": "a132b1b8f10b318d8e23aee15dab4caa14528aeb3c89966d4bcc25fb54af72ad"
},
- {
- "name": "family_mmgb",
- "unicode": "1F468-1F468-1F467-1F466",
+ "family_mmgb": {
+ "category": "people",
+ "moji": "👨‍👨‍👧‍👦",
+ "unicodeVersion": "6.0",
"digest": "eb2bc1966df406aaf38ce5a58db9324162799cdacf31f74f40e6384807a8efc2"
},
- {
- "name": "family_mmgg",
- "unicode": "1F468-1F468-1F467-1F467",
+ "family_mmgg": {
+ "category": "people",
+ "moji": "👨‍👨‍👧‍👧",
+ "unicodeVersion": "6.0",
"digest": "24f3d60f98fbd6b687f7cacfb629390b90509a754036e5439ae5294759c0606b"
},
- {
- "name": "family_mwbb",
- "unicode": "1F468-1F469-1F466-1F466",
+ "family_mwbb": {
+ "category": "people",
+ "moji": "👨‍👩‍👦‍👦",
+ "unicodeVersion": "6.0",
"digest": "2f77692bcb9275c4df501b64a18401dcaf8c68b21f26fbdad59b1feab0c98fd1"
},
- {
- "name": "family_mwg",
- "unicode": "1F468-1F469-1F467",
+ "family_mwg": {
+ "category": "people",
+ "moji": "👨‍👩‍👧",
+ "unicodeVersion": "6.0",
"digest": "1a976d13127665d9386cebfdb24e5572dc499bda484c0ee05585886edc616130"
},
- {
- "name": "family_mwgb",
- "unicode": "1F468-1F469-1F467-1F466",
+ "family_mwgb": {
+ "category": "people",
+ "moji": "👨‍👩‍👧‍👦",
+ "unicodeVersion": "6.0",
"digest": "960ec2cbac13ef208e73644cd36711b83e6c070c36950f834f3669812839b7f8"
},
- {
- "name": "family_mwgg",
- "unicode": "1F468-1F469-1F467-1F467",
+ "family_mwgg": {
+ "category": "people",
+ "moji": "👨‍👩‍👧‍👧",
+ "unicodeVersion": "6.0",
"digest": "8353b03dfa5c24aba75a0abdfdac01603f593819d54b4c7f2f88aafb31da0c6a"
},
- {
- "name": "family_wwb",
- "unicode": "1F469-1F469-1F466",
+ "family_wwb": {
+ "category": "people",
+ "moji": "👩‍👩‍👦",
+ "unicodeVersion": "6.0",
"digest": "07a5dd397718c553573689f6512f386729c13a12d5dc78be47c06405769cd98a"
},
- {
- "name": "family_wwbb",
- "unicode": "1F469-1F469-1F466-1F466",
+ "family_wwbb": {
+ "category": "people",
+ "moji": "👩‍👩‍👦‍👦",
+ "unicodeVersion": "6.0",
"digest": "b627f460f1da0d47b0b662402940b2b77c9538d380d05436dfca4b456c50c939"
},
- {
- "name": "family_wwg",
- "unicode": "1F469-1F469-1F467",
+ "family_wwg": {
+ "category": "people",
+ "moji": "👩‍👩‍👧",
+ "unicodeVersion": "6.0",
"digest": "2d6f373bed53f1028f0fbe9caf036465a351f37b9e00fca7d722cc5a1984f251"
},
- {
- "name": "family_wwgb",
- "unicode": "1F469-1F469-1F467-1F466",
+ "family_wwgb": {
+ "category": "people",
+ "moji": "👩‍👩‍👧‍👦",
+ "unicodeVersion": "6.0",
"digest": "72be5c85e1621f73d6794edd6e428febdb366b9e4c816f7829897fd1ab34642b"
},
- {
- "name": "family_wwgg",
- "unicode": "1F469-1F469-1F467-1F467",
+ "family_wwgg": {
+ "category": "people",
+ "moji": "👩‍👩‍👧‍👧",
+ "unicodeVersion": "6.0",
"digest": "c39e0916069460d2d9741bddf58e76f5d6a09254cba0eeb262345adf8630bc32"
},
- {
- "name": "fast_forward",
- "unicode": "23E9",
+ "fast_forward": {
+ "category": "symbols",
+ "moji": "⏩",
+ "unicodeVersion": "6.0",
"digest": "e7d2d8085cfd406c2b096e8dd147dd3722290a5727b1f7df185989526a2335ec"
},
- {
- "name": "fax",
- "unicode": "1F4E0",
+ "fax": {
+ "category": "objects",
+ "moji": "📠",
+ "unicodeVersion": "6.0",
"digest": "ff85ffa440c5379c9b138ebe2d7912d6098da3b37a051b80442d5557b7f993b0"
},
- {
- "name": "fearful",
- "unicode": "1F628",
+ "fearful": {
+ "category": "people",
+ "moji": "😨",
+ "unicodeVersion": "6.0",
"digest": "b72bdf7d075d5c4e38bbd8512fb45fda2e85c9c8732a47e67575ae9f2ed4c5df"
},
- {
- "name": "feet",
- "unicode": "1F43E",
+ "feet": {
+ "category": "nature",
+ "moji": "🐾",
+ "unicodeVersion": "6.0",
"digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016"
},
- {
- "name": "fencer",
- "unicode": "1F93A",
- "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
- },
- {
- "name": "fencing",
- "unicode": "1F93A",
+ "fencer": {
+ "category": "activity",
+ "moji": "🤺",
+ "unicodeVersion": "9.0",
"digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
},
- {
- "name": "ferris_wheel",
- "unicode": "1F3A1",
+ "ferris_wheel": {
+ "category": "travel",
+ "moji": "🎡",
+ "unicodeVersion": "6.0",
"digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c"
},
- {
- "name": "ferry",
- "unicode": "26F4",
+ "ferry": {
+ "category": "travel",
+ "moji": "⛴",
+ "unicodeVersion": "5.2",
"digest": "5002a72af2e3c4cef9a36ad5987aeed7d99f96bfd13e56f78957315ec7e749a3"
},
- {
- "name": "field_hockey",
- "unicode": "1F3D1",
+ "field_hockey": {
+ "category": "activity",
+ "moji": "🏑",
+ "unicodeVersion": "8.0",
"digest": "4ee091d96161ba719ab8fd6f2b03f96d902a6f22cffe0563b930618bb8ac2b67"
},
- {
- "name": "file_cabinet",
- "unicode": "1F5C4",
+ "file_cabinet": {
+ "category": "objects",
+ "moji": "🗄",
+ "unicodeVersion": "7.0",
"digest": "92914147bf93e6d64271ff99d217a18a9850a367d08a5f9f458ecf9311a5bbe9"
},
- {
- "name": "file_folder",
- "unicode": "1F4C1",
+ "file_folder": {
+ "category": "objects",
+ "moji": "📁",
+ "unicodeVersion": "6.0",
"digest": "62a42a929267cfbfdb795ead381c9657c343458bc5fca95ea8a0ab892c61d4f6"
},
- {
- "name": "film_frames",
- "unicode": "1F39E",
+ "film_frames": {
+ "category": "objects",
+ "moji": "🎞",
+ "unicodeVersion": "7.0",
"digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a"
},
- {
- "name": "fingers_crossed",
- "unicode": "1F91E",
- "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
- },
- {
- "name": "hand_with_index_and_middle_finger_crossed",
- "unicode": "1F91E",
+ "fingers_crossed": {
+ "category": "people",
+ "moji": "🤞",
+ "unicodeVersion": "9.0",
"digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
},
- {
- "name": "fingers_crossed_tone1",
- "unicode": "1F91E-1F3FB",
- "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
- },
- {
- "name": "hand_with_index_and_middle_fingers_crossed_tone1",
- "unicode": "1F91E-1F3FB",
+ "fingers_crossed_tone1": {
+ "category": "people",
+ "moji": "🤞🏻",
+ "unicodeVersion": "9.0",
"digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
},
- {
- "name": "fingers_crossed_tone2",
- "unicode": "1F91E-1F3FC",
- "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
- },
- {
- "name": "hand_with_index_and_middle_fingers_crossed_tone2",
- "unicode": "1F91E-1F3FC",
+ "fingers_crossed_tone2": {
+ "category": "people",
+ "moji": "🤞🏼",
+ "unicodeVersion": "9.0",
"digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
},
- {
- "name": "fingers_crossed_tone3",
- "unicode": "1F91E-1F3FD",
- "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
- },
- {
- "name": "hand_with_index_and_middle_fingers_crossed_tone3",
- "unicode": "1F91E-1F3FD",
+ "fingers_crossed_tone3": {
+ "category": "people",
+ "moji": "🤞🏽",
+ "unicodeVersion": "9.0",
"digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
},
- {
- "name": "fingers_crossed_tone4",
- "unicode": "1F91E-1F3FE",
- "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
- },
- {
- "name": "hand_with_index_and_middle_fingers_crossed_tone4",
- "unicode": "1F91E-1F3FE",
+ "fingers_crossed_tone4": {
+ "category": "people",
+ "moji": "🤞🏾",
+ "unicodeVersion": "9.0",
"digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
},
- {
- "name": "fingers_crossed_tone5",
- "unicode": "1F91E-1F3FF",
- "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
- },
- {
- "name": "hand_with_index_and_middle_fingers_crossed_tone5",
- "unicode": "1F91E-1F3FF",
+ "fingers_crossed_tone5": {
+ "category": "people",
+ "moji": "🤞🏿",
+ "unicodeVersion": "9.0",
"digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
},
- {
- "name": "fire",
- "unicode": "1F525",
+ "fire": {
+ "category": "nature",
+ "moji": "🔥",
+ "unicodeVersion": "6.0",
"digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
},
- {
- "name": "flame",
- "unicode": "1F525",
- "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416"
- },
- {
- "name": "fire_engine",
- "unicode": "1F692",
+ "fire_engine": {
+ "category": "travel",
+ "moji": "🚒",
+ "unicodeVersion": "6.0",
"digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20"
},
- {
- "name": "fireworks",
- "unicode": "1F386",
+ "fireworks": {
+ "category": "travel",
+ "moji": "🎆",
+ "unicodeVersion": "6.0",
"digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65"
},
- {
- "name": "first_place",
- "unicode": "1F947",
- "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
- },
- {
- "name": "first_place_medal",
- "unicode": "1F947",
+ "first_place": {
+ "category": "activity",
+ "moji": "🥇",
+ "unicodeVersion": "9.0",
"digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
},
- {
- "name": "first_quarter_moon",
- "unicode": "1F313",
+ "first_quarter_moon": {
+ "category": "nature",
+ "moji": "🌓",
+ "unicodeVersion": "6.0",
"digest": "a207ce93084448622a4a5c49c85c566a9fda6be7337c86a013eeb713fe47fd29"
},
- {
- "name": "first_quarter_moon_with_face",
- "unicode": "1F31B",
+ "first_quarter_moon_with_face": {
+ "category": "nature",
+ "moji": "🌛",
+ "unicodeVersion": "6.0",
"digest": "1d1f54a5075f2311bcc017c44898b9d8c58edc13b298d58c238fff9ab8ee2ef3"
},
- {
- "name": "fish",
- "unicode": "1F41F",
+ "fish": {
+ "category": "nature",
+ "moji": "🐟",
+ "unicodeVersion": "6.0",
"digest": "8f62f08fbeaf39694c19816b5c7d4f292017fe5bf9f8dd7e40f1630f5f83b28b"
},
- {
- "name": "fish_cake",
- "unicode": "1F365",
+ "fish_cake": {
+ "category": "food",
+ "moji": "🍥",
+ "unicodeVersion": "6.0",
"digest": "5a6ca2100c8830927b22afa6f1d2fc821f5692cd23507fe5a776f6e085cbbfb2"
},
- {
- "name": "fishing_pole_and_fish",
- "unicode": "1F3A3",
+ "fishing_pole_and_fish": {
+ "category": "activity",
+ "moji": "🎣",
+ "unicodeVersion": "6.0",
"digest": "f8fb84eccceec88321b0a2a46f732ecfc378f787c19c27ac1327735f1ca9a48b"
},
- {
- "name": "fist",
- "unicode": "270A",
+ "fist": {
+ "category": "people",
+ "moji": "✊",
+ "unicodeVersion": "6.0",
"digest": "557f96d85615b8d78436bc67266115bfc8556c97c14f7909dfda1cf134e8344f"
},
- {
- "name": "fist_tone1",
- "unicode": "270A-1F3FB",
+ "fist_tone1": {
+ "category": "people",
+ "moji": "✊🏻",
+ "unicodeVersion": "8.0",
"digest": "6c1b946f9e01abc39b5085e24e8b6077fc0e34188e8daa30c6a3adddd387413e"
},
- {
- "name": "fist_tone2",
- "unicode": "270A-1F3FC",
+ "fist_tone2": {
+ "category": "people",
+ "moji": "✊🏼",
+ "unicodeVersion": "8.0",
"digest": "e9b9e1ec638dca4d5e1519bca7338f58cce2f2a282ee4c3581e8643166fc415f"
},
- {
- "name": "fist_tone3",
- "unicode": "270A-1F3FD",
+ "fist_tone3": {
+ "category": "people",
+ "moji": "✊🏽",
+ "unicodeVersion": "8.0",
"digest": "8c14d24055c143960b3d2a27fe23c55d2d3ac5f84f87e4e876616235e8698c7f"
},
- {
- "name": "fist_tone4",
- "unicode": "270A-1F3FE",
+ "fist_tone4": {
+ "category": "people",
+ "moji": "✊🏾",
+ "unicodeVersion": "8.0",
"digest": "923f034f481e952e6e5d1664588f99f79bd5416d4197b0ade6621f2669ce5765"
},
- {
- "name": "fist_tone5",
- "unicode": "270A-1F3FF",
+ "fist_tone5": {
+ "category": "people",
+ "moji": "✊🏿",
+ "unicodeVersion": "8.0",
"digest": "d691d2902216080916a29047e07d7a5bf2aed07e062067ca9d01cbf6fdf48c8d"
},
- {
- "name": "five",
- "unicode": "0035-20E3",
+ "five": {
+ "category": "symbols",
+ "moji": "5️⃣",
+ "unicodeVersion": "3.0",
"digest": "8f03f62fdbf744ae49c8a60fbf715ebfccbd6b62d91148e0923907006f3c2726"
},
- {
- "name": "flag_ac",
- "unicode": "1F1E6-1F1E8",
- "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
- },
- {
- "name": "ac",
- "unicode": "1F1E6-1F1E8",
+ "flag_ac": {
+ "category": "flags",
+ "moji": "🇦🇨",
+ "unicodeVersion": "6.0",
"digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c"
},
- {
- "name": "flag_ad",
- "unicode": "1F1E6-1F1E9",
+ "flag_ad": {
+ "category": "flags",
+ "moji": "🇦🇩",
+ "unicodeVersion": "6.0",
"digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
},
- {
- "name": "ad",
- "unicode": "1F1E6-1F1E9",
- "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a"
- },
- {
- "name": "flag_ae",
- "unicode": "1F1E6-1F1EA",
- "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
- },
- {
- "name": "ae",
- "unicode": "1F1E6-1F1EA",
+ "flag_ae": {
+ "category": "flags",
+ "moji": "🇦🇪",
+ "unicodeVersion": "6.0",
"digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e"
},
- {
- "name": "flag_af",
- "unicode": "1F1E6-1F1EB",
+ "flag_af": {
+ "category": "flags",
+ "moji": "🇦🇫",
+ "unicodeVersion": "6.0",
"digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
},
- {
- "name": "af",
- "unicode": "1F1E6-1F1EB",
- "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3"
- },
- {
- "name": "flag_ag",
- "unicode": "1F1E6-1F1EC",
- "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
- },
- {
- "name": "ag",
- "unicode": "1F1E6-1F1EC",
+ "flag_ag": {
+ "category": "flags",
+ "moji": "🇦🇬",
+ "unicodeVersion": "6.0",
"digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4"
},
- {
- "name": "flag_ai",
- "unicode": "1F1E6-1F1EE",
+ "flag_ai": {
+ "category": "flags",
+ "moji": "🇦🇮",
+ "unicodeVersion": "6.0",
"digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
},
- {
- "name": "ai",
- "unicode": "1F1E6-1F1EE",
- "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f"
- },
- {
- "name": "flag_al",
- "unicode": "1F1E6-1F1F1",
- "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
- },
- {
- "name": "al",
- "unicode": "1F1E6-1F1F1",
+ "flag_al": {
+ "category": "flags",
+ "moji": "🇦🇱",
+ "unicodeVersion": "6.0",
"digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea"
},
- {
- "name": "flag_am",
- "unicode": "1F1E6-1F1F2",
+ "flag_am": {
+ "category": "flags",
+ "moji": "🇦🇲",
+ "unicodeVersion": "6.0",
"digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
},
- {
- "name": "am",
- "unicode": "1F1E6-1F1F2",
- "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8"
- },
- {
- "name": "flag_ao",
- "unicode": "1F1E6-1F1F4",
+ "flag_ao": {
+ "category": "flags",
+ "moji": "🇦🇴",
+ "unicodeVersion": "6.0",
"digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
},
- {
- "name": "ao",
- "unicode": "1F1E6-1F1F4",
- "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a"
- },
- {
- "name": "flag_aq",
- "unicode": "1F1E6-1F1F6",
+ "flag_aq": {
+ "category": "flags",
+ "moji": "🇦🇶",
+ "unicodeVersion": "6.0",
"digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
},
- {
- "name": "aq",
- "unicode": "1F1E6-1F1F6",
- "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa"
- },
- {
- "name": "flag_ar",
- "unicode": "1F1E6-1F1F7",
- "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
- },
- {
- "name": "ar",
- "unicode": "1F1E6-1F1F7",
+ "flag_ar": {
+ "category": "flags",
+ "moji": "🇦🇷",
+ "unicodeVersion": "6.0",
"digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25"
},
- {
- "name": "flag_as",
- "unicode": "1F1E6-1F1F8",
+ "flag_as": {
+ "category": "flags",
+ "moji": "🇦🇸",
+ "unicodeVersion": "6.0",
"digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
},
- {
- "name": "as",
- "unicode": "1F1E6-1F1F8",
- "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6"
- },
- {
- "name": "flag_at",
- "unicode": "1F1E6-1F1F9",
- "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
- },
- {
- "name": "at",
- "unicode": "1F1E6-1F1F9",
+ "flag_at": {
+ "category": "flags",
+ "moji": "🇦🇹",
+ "unicodeVersion": "6.0",
"digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217"
},
- {
- "name": "flag_au",
- "unicode": "1F1E6-1F1FA",
- "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
- },
- {
- "name": "au",
- "unicode": "1F1E6-1F1FA",
+ "flag_au": {
+ "category": "flags",
+ "moji": "🇦🇺",
+ "unicodeVersion": "6.0",
"digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd"
},
- {
- "name": "flag_aw",
- "unicode": "1F1E6-1F1FC",
- "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
- },
- {
- "name": "aw",
- "unicode": "1F1E6-1F1FC",
+ "flag_aw": {
+ "category": "flags",
+ "moji": "🇦🇼",
+ "unicodeVersion": "6.0",
"digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c"
},
- {
- "name": "flag_ax",
- "unicode": "1F1E6-1F1FD",
+ "flag_ax": {
+ "category": "flags",
+ "moji": "🇦🇽",
+ "unicodeVersion": "6.0",
"digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
},
- {
- "name": "ax",
- "unicode": "1F1E6-1F1FD",
- "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf"
- },
- {
- "name": "flag_az",
- "unicode": "1F1E6-1F1FF",
- "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
- },
- {
- "name": "az",
- "unicode": "1F1E6-1F1FF",
+ "flag_az": {
+ "category": "flags",
+ "moji": "🇦🇿",
+ "unicodeVersion": "6.0",
"digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2"
},
- {
- "name": "flag_ba",
- "unicode": "1F1E7-1F1E6",
- "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
- },
- {
- "name": "ba",
- "unicode": "1F1E7-1F1E6",
+ "flag_ba": {
+ "category": "flags",
+ "moji": "🇧🇦",
+ "unicodeVersion": "6.0",
"digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828"
},
- {
- "name": "flag_bb",
- "unicode": "1F1E7-1F1E7",
+ "flag_bb": {
+ "category": "flags",
+ "moji": "🇧🇧",
+ "unicodeVersion": "6.0",
"digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
},
- {
- "name": "bb",
- "unicode": "1F1E7-1F1E7",
- "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9"
- },
- {
- "name": "flag_bd",
- "unicode": "1F1E7-1F1E9",
- "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
- },
- {
- "name": "bd",
- "unicode": "1F1E7-1F1E9",
+ "flag_bd": {
+ "category": "flags",
+ "moji": "🇧🇩",
+ "unicodeVersion": "6.0",
"digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665"
},
- {
- "name": "flag_be",
- "unicode": "1F1E7-1F1EA",
+ "flag_be": {
+ "category": "flags",
+ "moji": "🇧🇪",
+ "unicodeVersion": "6.0",
"digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
},
- {
- "name": "be",
- "unicode": "1F1E7-1F1EA",
- "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948"
- },
- {
- "name": "flag_bf",
- "unicode": "1F1E7-1F1EB",
- "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
- },
- {
- "name": "bf",
- "unicode": "1F1E7-1F1EB",
+ "flag_bf": {
+ "category": "flags",
+ "moji": "🇧🇫",
+ "unicodeVersion": "6.0",
"digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0"
},
- {
- "name": "flag_bg",
- "unicode": "1F1E7-1F1EC",
+ "flag_bg": {
+ "category": "flags",
+ "moji": "🇧🇬",
+ "unicodeVersion": "6.0",
"digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
},
- {
- "name": "bg",
- "unicode": "1F1E7-1F1EC",
- "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18"
- },
- {
- "name": "flag_bh",
- "unicode": "1F1E7-1F1ED",
- "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
- },
- {
- "name": "bh",
- "unicode": "1F1E7-1F1ED",
+ "flag_bh": {
+ "category": "flags",
+ "moji": "🇧🇭",
+ "unicodeVersion": "6.0",
"digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737"
},
- {
- "name": "flag_bi",
- "unicode": "1F1E7-1F1EE",
+ "flag_bi": {
+ "category": "flags",
+ "moji": "🇧🇮",
+ "unicodeVersion": "6.0",
"digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
},
- {
- "name": "bi",
- "unicode": "1F1E7-1F1EE",
- "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e"
- },
- {
- "name": "flag_bj",
- "unicode": "1F1E7-1F1EF",
- "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
- },
- {
- "name": "bj",
- "unicode": "1F1E7-1F1EF",
+ "flag_bj": {
+ "category": "flags",
+ "moji": "🇧🇯",
+ "unicodeVersion": "6.0",
"digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436"
},
- {
- "name": "flag_bl",
- "unicode": "1F1E7-1F1F1",
+ "flag_bl": {
+ "category": "flags",
+ "moji": "🇧🇱",
+ "unicodeVersion": "6.0",
"digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
},
- {
- "name": "bl",
- "unicode": "1F1E7-1F1F1",
- "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7"
- },
- {
- "name": "flag_black",
- "unicode": "1F3F4",
- "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
- },
- {
- "name": "waving_black_flag",
- "unicode": "1F3F4",
+ "flag_black": {
+ "category": "objects",
+ "moji": "🏴",
+ "unicodeVersion": "6.0",
"digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203"
},
- {
- "name": "flag_bm",
- "unicode": "1F1E7-1F1F2",
+ "flag_bm": {
+ "category": "flags",
+ "moji": "🇧🇲",
+ "unicodeVersion": "6.0",
"digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
},
- {
- "name": "bm",
- "unicode": "1F1E7-1F1F2",
- "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b"
- },
- {
- "name": "flag_bn",
- "unicode": "1F1E7-1F1F3",
- "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
- },
- {
- "name": "bn",
- "unicode": "1F1E7-1F1F3",
+ "flag_bn": {
+ "category": "flags",
+ "moji": "🇧🇳",
+ "unicodeVersion": "6.0",
"digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228"
},
- {
- "name": "flag_bo",
- "unicode": "1F1E7-1F1F4",
+ "flag_bo": {
+ "category": "flags",
+ "moji": "🇧🇴",
+ "unicodeVersion": "6.0",
"digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
},
- {
- "name": "bo",
- "unicode": "1F1E7-1F1F4",
- "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c"
- },
- {
- "name": "flag_bq",
- "unicode": "1F1E7-1F1F6",
- "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
- },
- {
- "name": "bq",
- "unicode": "1F1E7-1F1F6",
+ "flag_bq": {
+ "category": "flags",
+ "moji": "🇧🇶",
+ "unicodeVersion": "6.0",
"digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f"
},
- {
- "name": "flag_br",
- "unicode": "1F1E7-1F1F7",
+ "flag_br": {
+ "category": "flags",
+ "moji": "🇧🇷",
+ "unicodeVersion": "6.0",
"digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
},
- {
- "name": "br",
- "unicode": "1F1E7-1F1F7",
- "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0"
- },
- {
- "name": "flag_bs",
- "unicode": "1F1E7-1F1F8",
+ "flag_bs": {
+ "category": "flags",
+ "moji": "🇧🇸",
+ "unicodeVersion": "6.0",
"digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
},
- {
- "name": "bs",
- "unicode": "1F1E7-1F1F8",
- "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5"
- },
- {
- "name": "flag_bt",
- "unicode": "1F1E7-1F1F9",
+ "flag_bt": {
+ "category": "flags",
+ "moji": "🇧🇹",
+ "unicodeVersion": "6.0",
"digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
},
- {
- "name": "bt",
- "unicode": "1F1E7-1F1F9",
- "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba"
- },
- {
- "name": "flag_bv",
- "unicode": "1F1E7-1F1FB",
- "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
- },
- {
- "name": "bv",
- "unicode": "1F1E7-1F1FB",
+ "flag_bv": {
+ "category": "flags",
+ "moji": "🇧🇻",
+ "unicodeVersion": "6.0",
"digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
- {
- "name": "flag_bw",
- "unicode": "1F1E7-1F1FC",
+ "flag_bw": {
+ "category": "flags",
+ "moji": "🇧🇼",
+ "unicodeVersion": "6.0",
"digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
},
- {
- "name": "bw",
- "unicode": "1F1E7-1F1FC",
- "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f"
- },
- {
- "name": "flag_by",
- "unicode": "1F1E7-1F1FE",
- "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
- },
- {
- "name": "by",
- "unicode": "1F1E7-1F1FE",
+ "flag_by": {
+ "category": "flags",
+ "moji": "🇧🇾",
+ "unicodeVersion": "6.0",
"digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9"
},
- {
- "name": "flag_bz",
- "unicode": "1F1E7-1F1FF",
- "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
- },
- {
- "name": "bz",
- "unicode": "1F1E7-1F1FF",
+ "flag_bz": {
+ "category": "flags",
+ "moji": "🇧🇿",
+ "unicodeVersion": "6.0",
"digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a"
},
- {
- "name": "flag_ca",
- "unicode": "1F1E8-1F1E6",
- "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
- },
- {
- "name": "ca",
- "unicode": "1F1E8-1F1E6",
+ "flag_ca": {
+ "category": "flags",
+ "moji": "🇨🇦",
+ "unicodeVersion": "6.0",
"digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd"
},
- {
- "name": "flag_cc",
- "unicode": "1F1E8-1F1E8",
+ "flag_cc": {
+ "category": "flags",
+ "moji": "🇨🇨",
+ "unicodeVersion": "6.0",
"digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
},
- {
- "name": "cc",
- "unicode": "1F1E8-1F1E8",
- "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36"
- },
- {
- "name": "flag_cd",
- "unicode": "1F1E8-1F1E9",
- "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
- },
- {
- "name": "congo",
- "unicode": "1F1E8-1F1E9",
+ "flag_cd": {
+ "category": "flags",
+ "moji": "🇨🇩",
+ "unicodeVersion": "6.0",
"digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f"
},
- {
- "name": "flag_cf",
- "unicode": "1F1E8-1F1EB",
- "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
- },
- {
- "name": "cf",
- "unicode": "1F1E8-1F1EB",
+ "flag_cf": {
+ "category": "flags",
+ "moji": "🇨🇫",
+ "unicodeVersion": "6.0",
"digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228"
},
- {
- "name": "flag_cg",
- "unicode": "1F1E8-1F1EC",
- "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
- },
- {
- "name": "cg",
- "unicode": "1F1E8-1F1EC",
+ "flag_cg": {
+ "category": "flags",
+ "moji": "🇨🇬",
+ "unicodeVersion": "6.0",
"digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813"
},
- {
- "name": "flag_ch",
- "unicode": "1F1E8-1F1ED",
- "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
- },
- {
- "name": "ch",
- "unicode": "1F1E8-1F1ED",
+ "flag_ch": {
+ "category": "flags",
+ "moji": "🇨🇭",
+ "unicodeVersion": "6.0",
"digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
},
- {
- "name": "flag_ci",
- "unicode": "1F1E8-1F1EE",
- "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
- },
- {
- "name": "ci",
- "unicode": "1F1E8-1F1EE",
+ "flag_ci": {
+ "category": "flags",
+ "moji": "🇨🇮",
+ "unicodeVersion": "6.0",
"digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e"
},
- {
- "name": "flag_ck",
- "unicode": "1F1E8-1F1F0",
- "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
- },
- {
- "name": "ck",
- "unicode": "1F1E8-1F1F0",
+ "flag_ck": {
+ "category": "flags",
+ "moji": "🇨🇰",
+ "unicodeVersion": "6.0",
"digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136"
},
- {
- "name": "flag_cl",
- "unicode": "1F1E8-1F1F1",
- "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
- },
- {
- "name": "chile",
- "unicode": "1F1E8-1F1F1",
+ "flag_cl": {
+ "category": "flags",
+ "moji": "🇨🇱",
+ "unicodeVersion": "6.0",
"digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723"
},
- {
- "name": "flag_cm",
- "unicode": "1F1E8-1F1F2",
- "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
- },
- {
- "name": "cm",
- "unicode": "1F1E8-1F1F2",
+ "flag_cm": {
+ "category": "flags",
+ "moji": "🇨🇲",
+ "unicodeVersion": "6.0",
"digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad"
},
- {
- "name": "flag_cn",
- "unicode": "1F1E8-1F1F3",
- "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
- },
- {
- "name": "cn",
- "unicode": "1F1E8-1F1F3",
+ "flag_cn": {
+ "category": "flags",
+ "moji": "🇨🇳",
+ "unicodeVersion": "6.0",
"digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890"
},
- {
- "name": "flag_co",
- "unicode": "1F1E8-1F1F4",
- "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
- },
- {
- "name": "co",
- "unicode": "1F1E8-1F1F4",
+ "flag_co": {
+ "category": "flags",
+ "moji": "🇨🇴",
+ "unicodeVersion": "6.0",
"digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f"
},
- {
- "name": "flag_cp",
- "unicode": "1F1E8-1F1F5",
+ "flag_cp": {
+ "category": "flags",
+ "moji": "🇨🇵",
+ "unicodeVersion": "6.0",
"digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
- {
- "name": "cp",
- "unicode": "1F1E8-1F1F5",
- "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
- },
- {
- "name": "flag_cr",
- "unicode": "1F1E8-1F1F7",
- "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
- },
- {
- "name": "cr",
- "unicode": "1F1E8-1F1F7",
+ "flag_cr": {
+ "category": "flags",
+ "moji": "🇨🇷",
+ "unicodeVersion": "6.0",
"digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196"
},
- {
- "name": "flag_cu",
- "unicode": "1F1E8-1F1FA",
+ "flag_cu": {
+ "category": "flags",
+ "moji": "🇨🇺",
+ "unicodeVersion": "6.0",
"digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
},
- {
- "name": "cu",
- "unicode": "1F1E8-1F1FA",
- "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150"
- },
- {
- "name": "flag_cv",
- "unicode": "1F1E8-1F1FB",
- "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
- },
- {
- "name": "cv",
- "unicode": "1F1E8-1F1FB",
+ "flag_cv": {
+ "category": "flags",
+ "moji": "🇨🇻",
+ "unicodeVersion": "6.0",
"digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7"
},
- {
- "name": "flag_cw",
- "unicode": "1F1E8-1F1FC",
+ "flag_cw": {
+ "category": "flags",
+ "moji": "🇨🇼",
+ "unicodeVersion": "6.0",
"digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
},
- {
- "name": "cw",
- "unicode": "1F1E8-1F1FC",
- "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b"
- },
- {
- "name": "flag_cx",
- "unicode": "1F1E8-1F1FD",
- "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
- },
- {
- "name": "cx",
- "unicode": "1F1E8-1F1FD",
+ "flag_cx": {
+ "category": "flags",
+ "moji": "🇨🇽",
+ "unicodeVersion": "6.0",
"digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345"
},
- {
- "name": "flag_cy",
- "unicode": "1F1E8-1F1FE",
+ "flag_cy": {
+ "category": "flags",
+ "moji": "🇨🇾",
+ "unicodeVersion": "6.0",
"digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
},
- {
- "name": "cy",
- "unicode": "1F1E8-1F1FE",
- "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f"
- },
- {
- "name": "flag_cz",
- "unicode": "1F1E8-1F1FF",
- "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
- },
- {
- "name": "cz",
- "unicode": "1F1E8-1F1FF",
+ "flag_cz": {
+ "category": "flags",
+ "moji": "🇨🇿",
+ "unicodeVersion": "6.0",
"digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a"
},
- {
- "name": "flag_de",
- "unicode": "1F1E9-1F1EA",
+ "flag_de": {
+ "category": "flags",
+ "moji": "🇩🇪",
+ "unicodeVersion": "6.0",
"digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
},
- {
- "name": "de",
- "unicode": "1F1E9-1F1EA",
- "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1"
- },
- {
- "name": "flag_dg",
- "unicode": "1F1E9-1F1EC",
- "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
- },
- {
- "name": "dg",
- "unicode": "1F1E9-1F1EC",
+ "flag_dg": {
+ "category": "flags",
+ "moji": "🇩🇬",
+ "unicodeVersion": "6.0",
"digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
- {
- "name": "flag_dj",
- "unicode": "1F1E9-1F1EF",
+ "flag_dj": {
+ "category": "flags",
+ "moji": "🇩🇯",
+ "unicodeVersion": "6.0",
"digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
},
- {
- "name": "dj",
- "unicode": "1F1E9-1F1EF",
- "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd"
- },
- {
- "name": "flag_dk",
- "unicode": "1F1E9-1F1F0",
- "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
- },
- {
- "name": "dk",
- "unicode": "1F1E9-1F1F0",
+ "flag_dk": {
+ "category": "flags",
+ "moji": "🇩🇰",
+ "unicodeVersion": "6.0",
"digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77"
},
- {
- "name": "flag_dm",
- "unicode": "1F1E9-1F1F2",
+ "flag_dm": {
+ "category": "flags",
+ "moji": "🇩🇲",
+ "unicodeVersion": "6.0",
"digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
},
- {
- "name": "dm",
- "unicode": "1F1E9-1F1F2",
- "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb"
- },
- {
- "name": "flag_do",
- "unicode": "1F1E9-1F1F4",
- "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
- },
- {
- "name": "do",
- "unicode": "1F1E9-1F1F4",
+ "flag_do": {
+ "category": "flags",
+ "moji": "🇩🇴",
+ "unicodeVersion": "6.0",
"digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102"
},
- {
- "name": "flag_dz",
- "unicode": "1F1E9-1F1FF",
+ "flag_dz": {
+ "category": "flags",
+ "moji": "🇩🇿",
+ "unicodeVersion": "6.0",
"digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
},
- {
- "name": "dz",
- "unicode": "1F1E9-1F1FF",
- "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed"
- },
- {
- "name": "flag_ea",
- "unicode": "1F1EA-1F1E6",
+ "flag_ea": {
+ "category": "flags",
+ "moji": "🇪🇦",
+ "unicodeVersion": "6.0",
"digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
},
- {
- "name": "ea",
- "unicode": "1F1EA-1F1E6",
- "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d"
- },
- {
- "name": "flag_ec",
- "unicode": "1F1EA-1F1E8",
+ "flag_ec": {
+ "category": "flags",
+ "moji": "🇪🇨",
+ "unicodeVersion": "6.0",
"digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
},
- {
- "name": "ec",
- "unicode": "1F1EA-1F1E8",
- "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac"
- },
- {
- "name": "flag_ee",
- "unicode": "1F1EA-1F1EA",
- "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
- },
- {
- "name": "ee",
- "unicode": "1F1EA-1F1EA",
+ "flag_ee": {
+ "category": "flags",
+ "moji": "🇪🇪",
+ "unicodeVersion": "6.0",
"digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe"
},
- {
- "name": "flag_eg",
- "unicode": "1F1EA-1F1EC",
+ "flag_eg": {
+ "category": "flags",
+ "moji": "🇪🇬",
+ "unicodeVersion": "6.0",
"digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
},
- {
- "name": "eg",
- "unicode": "1F1EA-1F1EC",
- "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2"
- },
- {
- "name": "flag_eh",
- "unicode": "1F1EA-1F1ED",
- "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
- },
- {
- "name": "eh",
- "unicode": "1F1EA-1F1ED",
+ "flag_eh": {
+ "category": "flags",
+ "moji": "🇪🇭",
+ "unicodeVersion": "6.0",
"digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e"
},
- {
- "name": "flag_er",
- "unicode": "1F1EA-1F1F7",
- "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
- },
- {
- "name": "er",
- "unicode": "1F1EA-1F1F7",
+ "flag_er": {
+ "category": "flags",
+ "moji": "🇪🇷",
+ "unicodeVersion": "6.0",
"digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc"
},
- {
- "name": "flag_es",
- "unicode": "1F1EA-1F1F8",
- "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
- },
- {
- "name": "es",
- "unicode": "1F1EA-1F1F8",
+ "flag_es": {
+ "category": "flags",
+ "moji": "🇪🇸",
+ "unicodeVersion": "6.0",
"digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25"
},
- {
- "name": "flag_et",
- "unicode": "1F1EA-1F1F9",
- "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
- },
- {
- "name": "et",
- "unicode": "1F1EA-1F1F9",
+ "flag_et": {
+ "category": "flags",
+ "moji": "🇪🇹",
+ "unicodeVersion": "6.0",
"digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617"
},
- {
- "name": "flag_eu",
- "unicode": "1F1EA-1F1FA",
- "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
- },
- {
- "name": "eu",
- "unicode": "1F1EA-1F1FA",
+ "flag_eu": {
+ "category": "flags",
+ "moji": "🇪🇺",
+ "unicodeVersion": "6.0",
"digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4"
},
- {
- "name": "flag_fi",
- "unicode": "1F1EB-1F1EE",
- "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
- },
- {
- "name": "fi",
- "unicode": "1F1EB-1F1EE",
+ "flag_fi": {
+ "category": "flags",
+ "moji": "🇫🇮",
+ "unicodeVersion": "6.0",
"digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e"
},
- {
- "name": "flag_fj",
- "unicode": "1F1EB-1F1EF",
+ "flag_fj": {
+ "category": "flags",
+ "moji": "🇫🇯",
+ "unicodeVersion": "6.0",
"digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
},
- {
- "name": "fj",
- "unicode": "1F1EB-1F1EF",
- "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45"
- },
- {
- "name": "flag_fk",
- "unicode": "1F1EB-1F1F0",
- "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
- },
- {
- "name": "fk",
- "unicode": "1F1EB-1F1F0",
+ "flag_fk": {
+ "category": "flags",
+ "moji": "🇫🇰",
+ "unicodeVersion": "6.0",
"digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15"
},
- {
- "name": "flag_fm",
- "unicode": "1F1EB-1F1F2",
+ "flag_fm": {
+ "category": "flags",
+ "moji": "🇫🇲",
+ "unicodeVersion": "6.0",
"digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
},
- {
- "name": "fm",
- "unicode": "1F1EB-1F1F2",
- "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990"
- },
- {
- "name": "flag_fo",
- "unicode": "1F1EB-1F1F4",
- "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
- },
- {
- "name": "fo",
- "unicode": "1F1EB-1F1F4",
+ "flag_fo": {
+ "category": "flags",
+ "moji": "🇫🇴",
+ "unicodeVersion": "6.0",
"digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e"
},
- {
- "name": "flag_fr",
- "unicode": "1F1EB-1F1F7",
+ "flag_fr": {
+ "category": "flags",
+ "moji": "🇫🇷",
+ "unicodeVersion": "6.0",
"digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
},
- {
- "name": "fr",
- "unicode": "1F1EB-1F1F7",
- "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e"
- },
- {
- "name": "flag_ga",
- "unicode": "1F1EC-1F1E6",
- "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
- },
- {
- "name": "ga",
- "unicode": "1F1EC-1F1E6",
+ "flag_ga": {
+ "category": "flags",
+ "moji": "🇬🇦",
+ "unicodeVersion": "6.0",
"digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5"
},
- {
- "name": "flag_gb",
- "unicode": "1F1EC-1F1E7",
+ "flag_gb": {
+ "category": "flags",
+ "moji": "🇬🇧",
+ "unicodeVersion": "6.0",
"digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
},
- {
- "name": "gb",
- "unicode": "1F1EC-1F1E7",
- "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde"
- },
- {
- "name": "flag_gd",
- "unicode": "1F1EC-1F1E9",
+ "flag_gd": {
+ "category": "flags",
+ "moji": "🇬🇩",
+ "unicodeVersion": "6.0",
"digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
},
- {
- "name": "gd",
- "unicode": "1F1EC-1F1E9",
- "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f"
- },
- {
- "name": "flag_ge",
- "unicode": "1F1EC-1F1EA",
+ "flag_ge": {
+ "category": "flags",
+ "moji": "🇬🇪",
+ "unicodeVersion": "6.0",
"digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
},
- {
- "name": "ge",
- "unicode": "1F1EC-1F1EA",
- "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367"
- },
- {
- "name": "flag_gf",
- "unicode": "1F1EC-1F1EB",
- "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
- },
- {
- "name": "gf",
- "unicode": "1F1EC-1F1EB",
+ "flag_gf": {
+ "category": "flags",
+ "moji": "🇬🇫",
+ "unicodeVersion": "6.0",
"digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956"
},
- {
- "name": "flag_gg",
- "unicode": "1F1EC-1F1EC",
+ "flag_gg": {
+ "category": "flags",
+ "moji": "🇬🇬",
+ "unicodeVersion": "6.0",
"digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
},
- {
- "name": "gg",
- "unicode": "1F1EC-1F1EC",
- "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d"
- },
- {
- "name": "flag_gh",
- "unicode": "1F1EC-1F1ED",
- "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
- },
- {
- "name": "gh",
- "unicode": "1F1EC-1F1ED",
+ "flag_gh": {
+ "category": "flags",
+ "moji": "🇬🇭",
+ "unicodeVersion": "6.0",
"digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26"
},
- {
- "name": "flag_gi",
- "unicode": "1F1EC-1F1EE",
- "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
- },
- {
- "name": "gi",
- "unicode": "1F1EC-1F1EE",
+ "flag_gi": {
+ "category": "flags",
+ "moji": "🇬🇮",
+ "unicodeVersion": "6.0",
"digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07"
},
- {
- "name": "flag_gl",
- "unicode": "1F1EC-1F1F1",
- "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
- },
- {
- "name": "gl",
- "unicode": "1F1EC-1F1F1",
+ "flag_gl": {
+ "category": "flags",
+ "moji": "🇬🇱",
+ "unicodeVersion": "6.0",
"digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b"
},
- {
- "name": "flag_gm",
- "unicode": "1F1EC-1F1F2",
+ "flag_gm": {
+ "category": "flags",
+ "moji": "🇬🇲",
+ "unicodeVersion": "6.0",
"digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
},
- {
- "name": "gm",
- "unicode": "1F1EC-1F1F2",
- "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1"
- },
- {
- "name": "flag_gn",
- "unicode": "1F1EC-1F1F3",
- "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
- },
- {
- "name": "gn",
- "unicode": "1F1EC-1F1F3",
+ "flag_gn": {
+ "category": "flags",
+ "moji": "🇬🇳",
+ "unicodeVersion": "6.0",
"digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558"
},
- {
- "name": "flag_gp",
- "unicode": "1F1EC-1F1F5",
- "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
- },
- {
- "name": "gp",
- "unicode": "1F1EC-1F1F5",
+ "flag_gp": {
+ "category": "flags",
+ "moji": "🇬🇵",
+ "unicodeVersion": "6.0",
"digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2"
},
- {
- "name": "flag_gq",
- "unicode": "1F1EC-1F1F6",
- "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
- },
- {
- "name": "gq",
- "unicode": "1F1EC-1F1F6",
+ "flag_gq": {
+ "category": "flags",
+ "moji": "🇬🇶",
+ "unicodeVersion": "6.0",
"digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70"
},
- {
- "name": "flag_gr",
- "unicode": "1F1EC-1F1F7",
- "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
- },
- {
- "name": "gr",
- "unicode": "1F1EC-1F1F7",
+ "flag_gr": {
+ "category": "flags",
+ "moji": "🇬🇷",
+ "unicodeVersion": "6.0",
"digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc"
},
- {
- "name": "flag_gs",
- "unicode": "1F1EC-1F1F8",
- "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
- },
- {
- "name": "gs",
- "unicode": "1F1EC-1F1F8",
+ "flag_gs": {
+ "category": "flags",
+ "moji": "🇬🇸",
+ "unicodeVersion": "6.0",
"digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9"
},
- {
- "name": "flag_gt",
- "unicode": "1F1EC-1F1F9",
- "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
- },
- {
- "name": "gt",
- "unicode": "1F1EC-1F1F9",
+ "flag_gt": {
+ "category": "flags",
+ "moji": "🇬🇹",
+ "unicodeVersion": "6.0",
"digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832"
},
- {
- "name": "flag_gu",
- "unicode": "1F1EC-1F1FA",
- "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
- },
- {
- "name": "gu",
- "unicode": "1F1EC-1F1FA",
+ "flag_gu": {
+ "category": "flags",
+ "moji": "🇬🇺",
+ "unicodeVersion": "6.0",
"digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3"
},
- {
- "name": "flag_gw",
- "unicode": "1F1EC-1F1FC",
+ "flag_gw": {
+ "category": "flags",
+ "moji": "🇬🇼",
+ "unicodeVersion": "6.0",
"digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
},
- {
- "name": "gw",
- "unicode": "1F1EC-1F1FC",
- "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72"
- },
- {
- "name": "flag_gy",
- "unicode": "1F1EC-1F1FE",
- "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
- },
- {
- "name": "gy",
- "unicode": "1F1EC-1F1FE",
+ "flag_gy": {
+ "category": "flags",
+ "moji": "🇬🇾",
+ "unicodeVersion": "6.0",
"digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6"
},
- {
- "name": "flag_hk",
- "unicode": "1F1ED-1F1F0",
- "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
- },
- {
- "name": "hk",
- "unicode": "1F1ED-1F1F0",
+ "flag_hk": {
+ "category": "flags",
+ "moji": "🇭🇰",
+ "unicodeVersion": "6.0",
"digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f"
},
- {
- "name": "flag_hm",
- "unicode": "1F1ED-1F1F2",
+ "flag_hm": {
+ "category": "flags",
+ "moji": "🇭🇲",
+ "unicodeVersion": "6.0",
"digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
},
- {
- "name": "hm",
- "unicode": "1F1ED-1F1F2",
- "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22"
- },
- {
- "name": "flag_hn",
- "unicode": "1F1ED-1F1F3",
- "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
- },
- {
- "name": "hn",
- "unicode": "1F1ED-1F1F3",
+ "flag_hn": {
+ "category": "flags",
+ "moji": "🇭🇳",
+ "unicodeVersion": "6.0",
"digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac"
},
- {
- "name": "flag_hr",
- "unicode": "1F1ED-1F1F7",
+ "flag_hr": {
+ "category": "flags",
+ "moji": "🇭🇷",
+ "unicodeVersion": "6.0",
"digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
},
- {
- "name": "hr",
- "unicode": "1F1ED-1F1F7",
- "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88"
- },
- {
- "name": "flag_ht",
- "unicode": "1F1ED-1F1F9",
- "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
- },
- {
- "name": "ht",
- "unicode": "1F1ED-1F1F9",
+ "flag_ht": {
+ "category": "flags",
+ "moji": "🇭🇹",
+ "unicodeVersion": "6.0",
"digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1"
},
- {
- "name": "flag_hu",
- "unicode": "1F1ED-1F1FA",
+ "flag_hu": {
+ "category": "flags",
+ "moji": "🇭🇺",
+ "unicodeVersion": "6.0",
"digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
},
- {
- "name": "hu",
- "unicode": "1F1ED-1F1FA",
- "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7"
- },
- {
- "name": "flag_ic",
- "unicode": "1F1EE-1F1E8",
- "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
- },
- {
- "name": "ic",
- "unicode": "1F1EE-1F1E8",
+ "flag_ic": {
+ "category": "flags",
+ "moji": "🇮🇨",
+ "unicodeVersion": "6.0",
"digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432"
},
- {
- "name": "flag_id",
- "unicode": "1F1EE-1F1E9",
+ "flag_id": {
+ "category": "flags",
+ "moji": "🇮🇩",
+ "unicodeVersion": "6.0",
"digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
},
- {
- "name": "indonesia",
- "unicode": "1F1EE-1F1E9",
- "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c"
- },
- {
- "name": "flag_ie",
- "unicode": "1F1EE-1F1EA",
- "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
- },
- {
- "name": "ie",
- "unicode": "1F1EE-1F1EA",
+ "flag_ie": {
+ "category": "flags",
+ "moji": "🇮🇪",
+ "unicodeVersion": "6.0",
"digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390"
},
- {
- "name": "flag_il",
- "unicode": "1F1EE-1F1F1",
+ "flag_il": {
+ "category": "flags",
+ "moji": "🇮🇱",
+ "unicodeVersion": "6.0",
"digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
},
- {
- "name": "il",
- "unicode": "1F1EE-1F1F1",
- "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8"
- },
- {
- "name": "flag_im",
- "unicode": "1F1EE-1F1F2",
- "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
- },
- {
- "name": "im",
- "unicode": "1F1EE-1F1F2",
+ "flag_im": {
+ "category": "flags",
+ "moji": "🇮🇲",
+ "unicodeVersion": "6.0",
"digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e"
},
- {
- "name": "flag_in",
- "unicode": "1F1EE-1F1F3",
+ "flag_in": {
+ "category": "flags",
+ "moji": "🇮🇳",
+ "unicodeVersion": "6.0",
"digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
},
- {
- "name": "in",
- "unicode": "1F1EE-1F1F3",
- "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd"
- },
- {
- "name": "flag_io",
- "unicode": "1F1EE-1F1F4",
- "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
- },
- {
- "name": "io",
- "unicode": "1F1EE-1F1F4",
+ "flag_io": {
+ "category": "flags",
+ "moji": "🇮🇴",
+ "unicodeVersion": "6.0",
"digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e"
},
- {
- "name": "flag_iq",
- "unicode": "1F1EE-1F1F6",
+ "flag_iq": {
+ "category": "flags",
+ "moji": "🇮🇶",
+ "unicodeVersion": "6.0",
"digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
},
- {
- "name": "iq",
- "unicode": "1F1EE-1F1F6",
- "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d"
- },
- {
- "name": "flag_ir",
- "unicode": "1F1EE-1F1F7",
- "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
- },
- {
- "name": "ir",
- "unicode": "1F1EE-1F1F7",
+ "flag_ir": {
+ "category": "flags",
+ "moji": "🇮🇷",
+ "unicodeVersion": "6.0",
"digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e"
},
- {
- "name": "flag_is",
- "unicode": "1F1EE-1F1F8",
+ "flag_is": {
+ "category": "flags",
+ "moji": "🇮🇸",
+ "unicodeVersion": "6.0",
"digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
},
- {
- "name": "is",
- "unicode": "1F1EE-1F1F8",
- "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456"
- },
- {
- "name": "flag_it",
- "unicode": "1F1EE-1F1F9",
+ "flag_it": {
+ "category": "flags",
+ "moji": "🇮🇹",
+ "unicodeVersion": "6.0",
"digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
},
- {
- "name": "it",
- "unicode": "1F1EE-1F1F9",
- "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e"
- },
- {
- "name": "flag_je",
- "unicode": "1F1EF-1F1EA",
+ "flag_je": {
+ "category": "flags",
+ "moji": "🇯🇪",
+ "unicodeVersion": "6.0",
"digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
},
- {
- "name": "je",
- "unicode": "1F1EF-1F1EA",
- "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc"
- },
- {
- "name": "flag_jm",
- "unicode": "1F1EF-1F1F2",
- "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
- },
- {
- "name": "jm",
- "unicode": "1F1EF-1F1F2",
+ "flag_jm": {
+ "category": "flags",
+ "moji": "🇯🇲",
+ "unicodeVersion": "6.0",
"digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211"
},
- {
- "name": "flag_jo",
- "unicode": "1F1EF-1F1F4",
+ "flag_jo": {
+ "category": "flags",
+ "moji": "🇯🇴",
+ "unicodeVersion": "6.0",
"digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
},
- {
- "name": "jo",
- "unicode": "1F1EF-1F1F4",
- "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178"
- },
- {
- "name": "flag_jp",
- "unicode": "1F1EF-1F1F5",
- "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
- },
- {
- "name": "jp",
- "unicode": "1F1EF-1F1F5",
+ "flag_jp": {
+ "category": "flags",
+ "moji": "🇯🇵",
+ "unicodeVersion": "6.0",
"digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e"
},
- {
- "name": "flag_ke",
- "unicode": "1F1F0-1F1EA",
- "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
- },
- {
- "name": "ke",
- "unicode": "1F1F0-1F1EA",
+ "flag_ke": {
+ "category": "flags",
+ "moji": "🇰🇪",
+ "unicodeVersion": "6.0",
"digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e"
},
- {
- "name": "flag_kg",
- "unicode": "1F1F0-1F1EC",
- "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
- },
- {
- "name": "kg",
- "unicode": "1F1F0-1F1EC",
+ "flag_kg": {
+ "category": "flags",
+ "moji": "🇰🇬",
+ "unicodeVersion": "6.0",
"digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f"
},
- {
- "name": "flag_kh",
- "unicode": "1F1F0-1F1ED",
+ "flag_kh": {
+ "category": "flags",
+ "moji": "🇰🇭",
+ "unicodeVersion": "6.0",
"digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
},
- {
- "name": "kh",
- "unicode": "1F1F0-1F1ED",
- "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080"
- },
- {
- "name": "flag_ki",
- "unicode": "1F1F0-1F1EE",
- "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
- },
- {
- "name": "ki",
- "unicode": "1F1F0-1F1EE",
+ "flag_ki": {
+ "category": "flags",
+ "moji": "🇰🇮",
+ "unicodeVersion": "6.0",
"digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0"
},
- {
- "name": "flag_km",
- "unicode": "1F1F0-1F1F2",
- "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
- },
- {
- "name": "km",
- "unicode": "1F1F0-1F1F2",
+ "flag_km": {
+ "category": "flags",
+ "moji": "🇰🇲",
+ "unicodeVersion": "6.0",
"digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b"
},
- {
- "name": "flag_kn",
- "unicode": "1F1F0-1F1F3",
- "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
- },
- {
- "name": "kn",
- "unicode": "1F1F0-1F1F3",
+ "flag_kn": {
+ "category": "flags",
+ "moji": "🇰🇳",
+ "unicodeVersion": "6.0",
"digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac"
},
- {
- "name": "flag_kp",
- "unicode": "1F1F0-1F1F5",
- "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
- },
- {
- "name": "kp",
- "unicode": "1F1F0-1F1F5",
+ "flag_kp": {
+ "category": "flags",
+ "moji": "🇰🇵",
+ "unicodeVersion": "6.0",
"digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729"
},
- {
- "name": "flag_kr",
- "unicode": "1F1F0-1F1F7",
- "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
- },
- {
- "name": "kr",
- "unicode": "1F1F0-1F1F7",
+ "flag_kr": {
+ "category": "flags",
+ "moji": "🇰🇷",
+ "unicodeVersion": "6.0",
"digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6"
},
- {
- "name": "flag_kw",
- "unicode": "1F1F0-1F1FC",
- "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
- },
- {
- "name": "kw",
- "unicode": "1F1F0-1F1FC",
+ "flag_kw": {
+ "category": "flags",
+ "moji": "🇰🇼",
+ "unicodeVersion": "6.0",
"digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d"
},
- {
- "name": "flag_ky",
- "unicode": "1F1F0-1F1FE",
- "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
- },
- {
- "name": "ky",
- "unicode": "1F1F0-1F1FE",
+ "flag_ky": {
+ "category": "flags",
+ "moji": "🇰🇾",
+ "unicodeVersion": "6.0",
"digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1"
},
- {
- "name": "flag_kz",
- "unicode": "1F1F0-1F1FF",
- "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
- },
- {
- "name": "kz",
- "unicode": "1F1F0-1F1FF",
+ "flag_kz": {
+ "category": "flags",
+ "moji": "🇰🇿",
+ "unicodeVersion": "6.0",
"digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1"
},
- {
- "name": "flag_la",
- "unicode": "1F1F1-1F1E6",
- "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
- },
- {
- "name": "la",
- "unicode": "1F1F1-1F1E6",
+ "flag_la": {
+ "category": "flags",
+ "moji": "🇱🇦",
+ "unicodeVersion": "6.0",
"digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd"
},
- {
- "name": "flag_lb",
- "unicode": "1F1F1-1F1E7",
- "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
- },
- {
- "name": "lb",
- "unicode": "1F1F1-1F1E7",
+ "flag_lb": {
+ "category": "flags",
+ "moji": "🇱🇧",
+ "unicodeVersion": "6.0",
"digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62"
},
- {
- "name": "flag_lc",
- "unicode": "1F1F1-1F1E8",
+ "flag_lc": {
+ "category": "flags",
+ "moji": "🇱🇨",
+ "unicodeVersion": "6.0",
"digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
},
- {
- "name": "lc",
- "unicode": "1F1F1-1F1E8",
- "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396"
- },
- {
- "name": "flag_li",
- "unicode": "1F1F1-1F1EE",
- "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
- },
- {
- "name": "li",
- "unicode": "1F1F1-1F1EE",
+ "flag_li": {
+ "category": "flags",
+ "moji": "🇱🇮",
+ "unicodeVersion": "6.0",
"digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633"
},
- {
- "name": "flag_lk",
- "unicode": "1F1F1-1F1F0",
+ "flag_lk": {
+ "category": "flags",
+ "moji": "🇱🇰",
+ "unicodeVersion": "6.0",
"digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
},
- {
- "name": "lk",
- "unicode": "1F1F1-1F1F0",
- "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5"
- },
- {
- "name": "flag_lr",
- "unicode": "1F1F1-1F1F7",
- "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
- },
- {
- "name": "lr",
- "unicode": "1F1F1-1F1F7",
+ "flag_lr": {
+ "category": "flags",
+ "moji": "🇱🇷",
+ "unicodeVersion": "6.0",
"digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be"
},
- {
- "name": "flag_ls",
- "unicode": "1F1F1-1F1F8",
+ "flag_ls": {
+ "category": "flags",
+ "moji": "🇱🇸",
+ "unicodeVersion": "6.0",
"digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
},
- {
- "name": "ls",
- "unicode": "1F1F1-1F1F8",
- "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db"
- },
- {
- "name": "flag_lt",
- "unicode": "1F1F1-1F1F9",
- "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
- },
- {
- "name": "lt",
- "unicode": "1F1F1-1F1F9",
+ "flag_lt": {
+ "category": "flags",
+ "moji": "🇱🇹",
+ "unicodeVersion": "6.0",
"digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9"
},
- {
- "name": "flag_lu",
- "unicode": "1F1F1-1F1FA",
+ "flag_lu": {
+ "category": "flags",
+ "moji": "🇱🇺",
+ "unicodeVersion": "6.0",
"digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
},
- {
- "name": "lu",
- "unicode": "1F1F1-1F1FA",
- "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d"
- },
- {
- "name": "flag_lv",
- "unicode": "1F1F1-1F1FB",
+ "flag_lv": {
+ "category": "flags",
+ "moji": "🇱🇻",
+ "unicodeVersion": "6.0",
"digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
},
- {
- "name": "lv",
- "unicode": "1F1F1-1F1FB",
- "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2"
- },
- {
- "name": "flag_ly",
- "unicode": "1F1F1-1F1FE",
+ "flag_ly": {
+ "category": "flags",
+ "moji": "🇱🇾",
+ "unicodeVersion": "6.0",
"digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
},
- {
- "name": "ly",
- "unicode": "1F1F1-1F1FE",
- "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44"
- },
- {
- "name": "flag_ma",
- "unicode": "1F1F2-1F1E6",
- "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
- },
- {
- "name": "ma",
- "unicode": "1F1F2-1F1E6",
+ "flag_ma": {
+ "category": "flags",
+ "moji": "🇲🇦",
+ "unicodeVersion": "6.0",
"digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e"
},
- {
- "name": "flag_mc",
- "unicode": "1F1F2-1F1E8",
+ "flag_mc": {
+ "category": "flags",
+ "moji": "🇲🇨",
+ "unicodeVersion": "6.0",
"digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
},
- {
- "name": "mc",
- "unicode": "1F1F2-1F1E8",
- "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f"
- },
- {
- "name": "flag_md",
- "unicode": "1F1F2-1F1E9",
- "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
- },
- {
- "name": "md",
- "unicode": "1F1F2-1F1E9",
+ "flag_md": {
+ "category": "flags",
+ "moji": "🇲🇩",
+ "unicodeVersion": "6.0",
"digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8"
},
- {
- "name": "flag_me",
- "unicode": "1F1F2-1F1EA",
- "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
- },
- {
- "name": "me",
- "unicode": "1F1F2-1F1EA",
+ "flag_me": {
+ "category": "flags",
+ "moji": "🇲🇪",
+ "unicodeVersion": "6.0",
"digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416"
},
- {
- "name": "flag_mf",
- "unicode": "1F1F2-1F1EB",
- "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
- },
- {
- "name": "mf",
- "unicode": "1F1F2-1F1EB",
+ "flag_mf": {
+ "category": "flags",
+ "moji": "🇲🇫",
+ "unicodeVersion": "6.0",
"digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
- {
- "name": "flag_mg",
- "unicode": "1F1F2-1F1EC",
+ "flag_mg": {
+ "category": "flags",
+ "moji": "🇲🇬",
+ "unicodeVersion": "6.0",
"digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
},
- {
- "name": "mg",
- "unicode": "1F1F2-1F1EC",
- "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b"
- },
- {
- "name": "flag_mh",
- "unicode": "1F1F2-1F1ED",
- "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
- },
- {
- "name": "mh",
- "unicode": "1F1F2-1F1ED",
+ "flag_mh": {
+ "category": "flags",
+ "moji": "🇲🇭",
+ "unicodeVersion": "6.0",
"digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7"
},
- {
- "name": "flag_mk",
- "unicode": "1F1F2-1F1F0",
- "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
- },
- {
- "name": "mk",
- "unicode": "1F1F2-1F1F0",
+ "flag_mk": {
+ "category": "flags",
+ "moji": "🇲🇰",
+ "unicodeVersion": "6.0",
"digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f"
},
- {
- "name": "flag_ml",
- "unicode": "1F1F2-1F1F1",
+ "flag_ml": {
+ "category": "flags",
+ "moji": "🇲🇱",
+ "unicodeVersion": "6.0",
"digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
},
- {
- "name": "ml",
- "unicode": "1F1F2-1F1F1",
- "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b"
- },
- {
- "name": "flag_mm",
- "unicode": "1F1F2-1F1F2",
- "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
- },
- {
- "name": "mm",
- "unicode": "1F1F2-1F1F2",
+ "flag_mm": {
+ "category": "flags",
+ "moji": "🇲🇲",
+ "unicodeVersion": "6.0",
"digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d"
},
- {
- "name": "flag_mn",
- "unicode": "1F1F2-1F1F3",
+ "flag_mn": {
+ "category": "flags",
+ "moji": "🇲🇳",
+ "unicodeVersion": "6.0",
"digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
},
- {
- "name": "mn",
- "unicode": "1F1F2-1F1F3",
- "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad"
- },
- {
- "name": "flag_mo",
- "unicode": "1F1F2-1F1F4",
- "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
- },
- {
- "name": "mo",
- "unicode": "1F1F2-1F1F4",
+ "flag_mo": {
+ "category": "flags",
+ "moji": "🇲🇴",
+ "unicodeVersion": "6.0",
"digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39"
},
- {
- "name": "flag_mp",
- "unicode": "1F1F2-1F1F5",
+ "flag_mp": {
+ "category": "flags",
+ "moji": "🇲🇵",
+ "unicodeVersion": "6.0",
"digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
},
- {
- "name": "mp",
- "unicode": "1F1F2-1F1F5",
- "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba"
- },
- {
- "name": "flag_mq",
- "unicode": "1F1F2-1F1F6",
- "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
- },
- {
- "name": "mq",
- "unicode": "1F1F2-1F1F6",
+ "flag_mq": {
+ "category": "flags",
+ "moji": "🇲🇶",
+ "unicodeVersion": "6.0",
"digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e"
},
- {
- "name": "flag_mr",
- "unicode": "1F1F2-1F1F7",
+ "flag_mr": {
+ "category": "flags",
+ "moji": "🇲🇷",
+ "unicodeVersion": "6.0",
"digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
},
- {
- "name": "mr",
- "unicode": "1F1F2-1F1F7",
- "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c"
- },
- {
- "name": "flag_ms",
- "unicode": "1F1F2-1F1F8",
- "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
- },
- {
- "name": "ms",
- "unicode": "1F1F2-1F1F8",
+ "flag_ms": {
+ "category": "flags",
+ "moji": "🇲🇸",
+ "unicodeVersion": "6.0",
"digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc"
},
- {
- "name": "flag_mt",
- "unicode": "1F1F2-1F1F9",
+ "flag_mt": {
+ "category": "flags",
+ "moji": "🇲🇹",
+ "unicodeVersion": "6.0",
"digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
},
- {
- "name": "mt",
- "unicode": "1F1F2-1F1F9",
- "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469"
- },
- {
- "name": "flag_mu",
- "unicode": "1F1F2-1F1FA",
- "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
- },
- {
- "name": "mu",
- "unicode": "1F1F2-1F1FA",
+ "flag_mu": {
+ "category": "flags",
+ "moji": "🇲🇺",
+ "unicodeVersion": "6.0",
"digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253"
},
- {
- "name": "flag_mv",
- "unicode": "1F1F2-1F1FB",
+ "flag_mv": {
+ "category": "flags",
+ "moji": "🇲🇻",
+ "unicodeVersion": "6.0",
"digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
},
- {
- "name": "mv",
- "unicode": "1F1F2-1F1FB",
- "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb"
- },
- {
- "name": "flag_mw",
- "unicode": "1F1F2-1F1FC",
- "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
- },
- {
- "name": "mw",
- "unicode": "1F1F2-1F1FC",
+ "flag_mw": {
+ "category": "flags",
+ "moji": "🇲🇼",
+ "unicodeVersion": "6.0",
"digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5"
},
- {
- "name": "flag_mx",
- "unicode": "1F1F2-1F1FD",
+ "flag_mx": {
+ "category": "flags",
+ "moji": "🇲🇽",
+ "unicodeVersion": "6.0",
"digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
},
- {
- "name": "mx",
- "unicode": "1F1F2-1F1FD",
- "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd"
- },
- {
- "name": "flag_my",
- "unicode": "1F1F2-1F1FE",
- "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
- },
- {
- "name": "my",
- "unicode": "1F1F2-1F1FE",
+ "flag_my": {
+ "category": "flags",
+ "moji": "🇲🇾",
+ "unicodeVersion": "6.0",
"digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef"
},
- {
- "name": "flag_mz",
- "unicode": "1F1F2-1F1FF",
+ "flag_mz": {
+ "category": "flags",
+ "moji": "🇲🇿",
+ "unicodeVersion": "6.0",
"digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
},
- {
- "name": "mz",
- "unicode": "1F1F2-1F1FF",
- "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97"
- },
- {
- "name": "flag_na",
- "unicode": "1F1F3-1F1E6",
+ "flag_na": {
+ "category": "flags",
+ "moji": "🇳🇦",
+ "unicodeVersion": "6.0",
"digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
},
- {
- "name": "na",
- "unicode": "1F1F3-1F1E6",
- "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601"
- },
- {
- "name": "flag_nc",
- "unicode": "1F1F3-1F1E8",
+ "flag_nc": {
+ "category": "flags",
+ "moji": "🇳🇨",
+ "unicodeVersion": "6.0",
"digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
},
- {
- "name": "nc",
- "unicode": "1F1F3-1F1E8",
- "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329"
- },
- {
- "name": "flag_ne",
- "unicode": "1F1F3-1F1EA",
- "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
- },
- {
- "name": "ne",
- "unicode": "1F1F3-1F1EA",
+ "flag_ne": {
+ "category": "flags",
+ "moji": "🇳🇪",
+ "unicodeVersion": "6.0",
"digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd"
},
- {
- "name": "flag_nf",
- "unicode": "1F1F3-1F1EB",
+ "flag_nf": {
+ "category": "flags",
+ "moji": "🇳🇫",
+ "unicodeVersion": "6.0",
"digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
},
- {
- "name": "nf",
- "unicode": "1F1F3-1F1EB",
- "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584"
- },
- {
- "name": "flag_ng",
- "unicode": "1F1F3-1F1EC",
- "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
- },
- {
- "name": "nigeria",
- "unicode": "1F1F3-1F1EC",
+ "flag_ng": {
+ "category": "flags",
+ "moji": "🇳🇬",
+ "unicodeVersion": "6.0",
"digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956"
},
- {
- "name": "flag_ni",
- "unicode": "1F1F3-1F1EE",
- "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
- },
- {
- "name": "ni",
- "unicode": "1F1F3-1F1EE",
+ "flag_ni": {
+ "category": "flags",
+ "moji": "🇳🇮",
+ "unicodeVersion": "6.0",
"digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710"
},
- {
- "name": "flag_nl",
- "unicode": "1F1F3-1F1F1",
- "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
- },
- {
- "name": "nl",
- "unicode": "1F1F3-1F1F1",
+ "flag_nl": {
+ "category": "flags",
+ "moji": "🇳🇱",
+ "unicodeVersion": "6.0",
"digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71"
},
- {
- "name": "flag_no",
- "unicode": "1F1F3-1F1F4",
+ "flag_no": {
+ "category": "flags",
+ "moji": "🇳🇴",
+ "unicodeVersion": "6.0",
"digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
},
- {
- "name": "no",
- "unicode": "1F1F3-1F1F4",
- "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef"
- },
- {
- "name": "flag_np",
- "unicode": "1F1F3-1F1F5",
- "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
- },
- {
- "name": "np",
- "unicode": "1F1F3-1F1F5",
+ "flag_np": {
+ "category": "flags",
+ "moji": "🇳🇵",
+ "unicodeVersion": "6.0",
"digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee"
},
- {
- "name": "flag_nr",
- "unicode": "1F1F3-1F1F7",
- "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
- },
- {
- "name": "nr",
- "unicode": "1F1F3-1F1F7",
+ "flag_nr": {
+ "category": "flags",
+ "moji": "🇳🇷",
+ "unicodeVersion": "6.0",
"digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec"
},
- {
- "name": "flag_nu",
- "unicode": "1F1F3-1F1FA",
- "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
- },
- {
- "name": "nu",
- "unicode": "1F1F3-1F1FA",
+ "flag_nu": {
+ "category": "flags",
+ "moji": "🇳🇺",
+ "unicodeVersion": "6.0",
"digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d"
},
- {
- "name": "flag_nz",
- "unicode": "1F1F3-1F1FF",
- "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
- },
- {
- "name": "nz",
- "unicode": "1F1F3-1F1FF",
+ "flag_nz": {
+ "category": "flags",
+ "moji": "🇳🇿",
+ "unicodeVersion": "6.0",
"digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75"
},
- {
- "name": "flag_om",
- "unicode": "1F1F4-1F1F2",
- "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
- },
- {
- "name": "om",
- "unicode": "1F1F4-1F1F2",
+ "flag_om": {
+ "category": "flags",
+ "moji": "🇴🇲",
+ "unicodeVersion": "6.0",
"digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee"
},
- {
- "name": "flag_pa",
- "unicode": "1F1F5-1F1E6",
- "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
- },
- {
- "name": "pa",
- "unicode": "1F1F5-1F1E6",
+ "flag_pa": {
+ "category": "flags",
+ "moji": "🇵🇦",
+ "unicodeVersion": "6.0",
"digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7"
},
- {
- "name": "flag_pe",
- "unicode": "1F1F5-1F1EA",
- "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
- },
- {
- "name": "pe",
- "unicode": "1F1F5-1F1EA",
+ "flag_pe": {
+ "category": "flags",
+ "moji": "🇵🇪",
+ "unicodeVersion": "6.0",
"digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1"
},
- {
- "name": "flag_pf",
- "unicode": "1F1F5-1F1EB",
- "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
- },
- {
- "name": "pf",
- "unicode": "1F1F5-1F1EB",
+ "flag_pf": {
+ "category": "flags",
+ "moji": "🇵🇫",
+ "unicodeVersion": "6.0",
"digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23"
},
- {
- "name": "flag_pg",
- "unicode": "1F1F5-1F1EC",
- "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
- },
- {
- "name": "pg",
- "unicode": "1F1F5-1F1EC",
+ "flag_pg": {
+ "category": "flags",
+ "moji": "🇵🇬",
+ "unicodeVersion": "6.0",
"digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7"
},
- {
- "name": "flag_ph",
- "unicode": "1F1F5-1F1ED",
- "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
- },
- {
- "name": "ph",
- "unicode": "1F1F5-1F1ED",
+ "flag_ph": {
+ "category": "flags",
+ "moji": "🇵🇭",
+ "unicodeVersion": "6.0",
"digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517"
},
- {
- "name": "flag_pk",
- "unicode": "1F1F5-1F1F0",
+ "flag_pk": {
+ "category": "flags",
+ "moji": "🇵🇰",
+ "unicodeVersion": "6.0",
"digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
},
- {
- "name": "pk",
- "unicode": "1F1F5-1F1F0",
- "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521"
- },
- {
- "name": "flag_pl",
- "unicode": "1F1F5-1F1F1",
- "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
- },
- {
- "name": "pl",
- "unicode": "1F1F5-1F1F1",
+ "flag_pl": {
+ "category": "flags",
+ "moji": "🇵🇱",
+ "unicodeVersion": "6.0",
"digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895"
},
- {
- "name": "flag_pm",
- "unicode": "1F1F5-1F1F2",
+ "flag_pm": {
+ "category": "flags",
+ "moji": "🇵🇲",
+ "unicodeVersion": "6.0",
"digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
},
- {
- "name": "pm",
- "unicode": "1F1F5-1F1F2",
- "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644"
- },
- {
- "name": "flag_pn",
- "unicode": "1F1F5-1F1F3",
- "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
- },
- {
- "name": "pn",
- "unicode": "1F1F5-1F1F3",
+ "flag_pn": {
+ "category": "flags",
+ "moji": "🇵🇳",
+ "unicodeVersion": "6.0",
"digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72"
},
- {
- "name": "flag_pr",
- "unicode": "1F1F5-1F1F7",
+ "flag_pr": {
+ "category": "flags",
+ "moji": "🇵🇷",
+ "unicodeVersion": "6.0",
"digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
},
- {
- "name": "pr",
- "unicode": "1F1F5-1F1F7",
- "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46"
- },
- {
- "name": "flag_ps",
- "unicode": "1F1F5-1F1F8",
- "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
- },
- {
- "name": "ps",
- "unicode": "1F1F5-1F1F8",
+ "flag_ps": {
+ "category": "flags",
+ "moji": "🇵🇸",
+ "unicodeVersion": "6.0",
"digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289"
},
- {
- "name": "flag_pt",
- "unicode": "1F1F5-1F1F9",
+ "flag_pt": {
+ "category": "flags",
+ "moji": "🇵🇹",
+ "unicodeVersion": "6.0",
"digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
},
- {
- "name": "pt",
- "unicode": "1F1F5-1F1F9",
- "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b"
- },
- {
- "name": "flag_pw",
- "unicode": "1F1F5-1F1FC",
- "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
- },
- {
- "name": "pw",
- "unicode": "1F1F5-1F1FC",
+ "flag_pw": {
+ "category": "flags",
+ "moji": "🇵🇼",
+ "unicodeVersion": "6.0",
"digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412"
},
- {
- "name": "flag_py",
- "unicode": "1F1F5-1F1FE",
+ "flag_py": {
+ "category": "flags",
+ "moji": "🇵🇾",
+ "unicodeVersion": "6.0",
"digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
},
- {
- "name": "py",
- "unicode": "1F1F5-1F1FE",
- "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6"
- },
- {
- "name": "flag_qa",
- "unicode": "1F1F6-1F1E6",
- "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
- },
- {
- "name": "qa",
- "unicode": "1F1F6-1F1E6",
+ "flag_qa": {
+ "category": "flags",
+ "moji": "🇶🇦",
+ "unicodeVersion": "6.0",
"digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d"
},
- {
- "name": "flag_re",
- "unicode": "1F1F7-1F1EA",
+ "flag_re": {
+ "category": "flags",
+ "moji": "🇷🇪",
+ "unicodeVersion": "6.0",
"digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
},
- {
- "name": "re",
- "unicode": "1F1F7-1F1EA",
- "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80"
- },
- {
- "name": "flag_ro",
- "unicode": "1F1F7-1F1F4",
- "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
- },
- {
- "name": "ro",
- "unicode": "1F1F7-1F1F4",
+ "flag_ro": {
+ "category": "flags",
+ "moji": "🇷🇴",
+ "unicodeVersion": "6.0",
"digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c"
},
- {
- "name": "flag_rs",
- "unicode": "1F1F7-1F1F8",
+ "flag_rs": {
+ "category": "flags",
+ "moji": "🇷🇸",
+ "unicodeVersion": "6.0",
"digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
},
- {
- "name": "rs",
- "unicode": "1F1F7-1F1F8",
- "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee"
- },
- {
- "name": "flag_ru",
- "unicode": "1F1F7-1F1FA",
- "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
- },
- {
- "name": "ru",
- "unicode": "1F1F7-1F1FA",
+ "flag_ru": {
+ "category": "flags",
+ "moji": "🇷🇺",
+ "unicodeVersion": "6.0",
"digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7"
},
- {
- "name": "flag_rw",
- "unicode": "1F1F7-1F1FC",
+ "flag_rw": {
+ "category": "flags",
+ "moji": "🇷🇼",
+ "unicodeVersion": "6.0",
"digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
},
- {
- "name": "rw",
- "unicode": "1F1F7-1F1FC",
- "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca"
- },
- {
- "name": "flag_sa",
- "unicode": "1F1F8-1F1E6",
+ "flag_sa": {
+ "category": "flags",
+ "moji": "🇸🇦",
+ "unicodeVersion": "6.0",
"digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
},
- {
- "name": "saudiarabia",
- "unicode": "1F1F8-1F1E6",
- "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
- },
- {
- "name": "saudi",
- "unicode": "1F1F8-1F1E6",
- "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7"
- },
- {
- "name": "flag_sb",
- "unicode": "1F1F8-1F1E7",
- "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
- },
- {
- "name": "sb",
- "unicode": "1F1F8-1F1E7",
+ "flag_sb": {
+ "category": "flags",
+ "moji": "🇸🇧",
+ "unicodeVersion": "6.0",
"digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc"
},
- {
- "name": "flag_sc",
- "unicode": "1F1F8-1F1E8",
+ "flag_sc": {
+ "category": "flags",
+ "moji": "🇸🇨",
+ "unicodeVersion": "6.0",
"digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
},
- {
- "name": "sc",
- "unicode": "1F1F8-1F1E8",
- "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056"
- },
- {
- "name": "flag_sd",
- "unicode": "1F1F8-1F1E9",
- "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
- },
- {
- "name": "sd",
- "unicode": "1F1F8-1F1E9",
+ "flag_sd": {
+ "category": "flags",
+ "moji": "🇸🇩",
+ "unicodeVersion": "6.0",
"digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885"
},
- {
- "name": "flag_se",
- "unicode": "1F1F8-1F1EA",
- "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
- },
- {
- "name": "se",
- "unicode": "1F1F8-1F1EA",
+ "flag_se": {
+ "category": "flags",
+ "moji": "🇸🇪",
+ "unicodeVersion": "6.0",
"digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a"
},
- {
- "name": "flag_sg",
- "unicode": "1F1F8-1F1EC",
- "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
- },
- {
- "name": "sg",
- "unicode": "1F1F8-1F1EC",
+ "flag_sg": {
+ "category": "flags",
+ "moji": "🇸🇬",
+ "unicodeVersion": "6.0",
"digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac"
},
- {
- "name": "flag_sh",
- "unicode": "1F1F8-1F1ED",
- "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
- },
- {
- "name": "sh",
- "unicode": "1F1F8-1F1ED",
+ "flag_sh": {
+ "category": "flags",
+ "moji": "🇸🇭",
+ "unicodeVersion": "6.0",
"digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d"
},
- {
- "name": "flag_si",
- "unicode": "1F1F8-1F1EE",
- "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
- },
- {
- "name": "si",
- "unicode": "1F1F8-1F1EE",
+ "flag_si": {
+ "category": "flags",
+ "moji": "🇸🇮",
+ "unicodeVersion": "6.0",
"digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3"
},
- {
- "name": "flag_sj",
- "unicode": "1F1F8-1F1EF",
- "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
- },
- {
- "name": "sj",
- "unicode": "1F1F8-1F1EF",
+ "flag_sj": {
+ "category": "flags",
+ "moji": "🇸🇯",
+ "unicodeVersion": "6.0",
"digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6"
},
- {
- "name": "flag_sk",
- "unicode": "1F1F8-1F1F0",
+ "flag_sk": {
+ "category": "flags",
+ "moji": "🇸🇰",
+ "unicodeVersion": "6.0",
"digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
},
- {
- "name": "sk",
- "unicode": "1F1F8-1F1F0",
- "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36"
- },
- {
- "name": "flag_sl",
- "unicode": "1F1F8-1F1F1",
- "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
- },
- {
- "name": "sl",
- "unicode": "1F1F8-1F1F1",
+ "flag_sl": {
+ "category": "flags",
+ "moji": "🇸🇱",
+ "unicodeVersion": "6.0",
"digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02"
},
- {
- "name": "flag_sm",
- "unicode": "1F1F8-1F1F2",
+ "flag_sm": {
+ "category": "flags",
+ "moji": "🇸🇲",
+ "unicodeVersion": "6.0",
"digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
},
- {
- "name": "sm",
- "unicode": "1F1F8-1F1F2",
- "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94"
- },
- {
- "name": "flag_sn",
- "unicode": "1F1F8-1F1F3",
- "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
- },
- {
- "name": "sn",
- "unicode": "1F1F8-1F1F3",
+ "flag_sn": {
+ "category": "flags",
+ "moji": "🇸🇳",
+ "unicodeVersion": "6.0",
"digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334"
},
- {
- "name": "flag_so",
- "unicode": "1F1F8-1F1F4",
+ "flag_so": {
+ "category": "flags",
+ "moji": "🇸🇴",
+ "unicodeVersion": "6.0",
"digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
},
- {
- "name": "so",
- "unicode": "1F1F8-1F1F4",
- "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c"
- },
- {
- "name": "flag_sr",
- "unicode": "1F1F8-1F1F7",
- "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
- },
- {
- "name": "sr",
- "unicode": "1F1F8-1F1F7",
+ "flag_sr": {
+ "category": "flags",
+ "moji": "🇸🇷",
+ "unicodeVersion": "6.0",
"digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1"
},
- {
- "name": "flag_ss",
- "unicode": "1F1F8-1F1F8",
+ "flag_ss": {
+ "category": "flags",
+ "moji": "🇸🇸",
+ "unicodeVersion": "6.0",
"digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
},
- {
- "name": "ss",
- "unicode": "1F1F8-1F1F8",
- "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d"
- },
- {
- "name": "flag_st",
- "unicode": "1F1F8-1F1F9",
+ "flag_st": {
+ "category": "flags",
+ "moji": "🇸🇹",
+ "unicodeVersion": "6.0",
"digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
},
- {
- "name": "st",
- "unicode": "1F1F8-1F1F9",
- "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330"
- },
- {
- "name": "flag_sv",
- "unicode": "1F1F8-1F1FB",
+ "flag_sv": {
+ "category": "flags",
+ "moji": "🇸🇻",
+ "unicodeVersion": "6.0",
"digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
},
- {
- "name": "sv",
- "unicode": "1F1F8-1F1FB",
- "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242"
- },
- {
- "name": "flag_sx",
- "unicode": "1F1F8-1F1FD",
- "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
- },
- {
- "name": "sx",
- "unicode": "1F1F8-1F1FD",
+ "flag_sx": {
+ "category": "flags",
+ "moji": "🇸🇽",
+ "unicodeVersion": "6.0",
"digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd"
},
- {
- "name": "flag_sy",
- "unicode": "1F1F8-1F1FE",
+ "flag_sy": {
+ "category": "flags",
+ "moji": "🇸🇾",
+ "unicodeVersion": "6.0",
"digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
},
- {
- "name": "sy",
- "unicode": "1F1F8-1F1FE",
- "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b"
- },
- {
- "name": "flag_sz",
- "unicode": "1F1F8-1F1FF",
- "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
- },
- {
- "name": "sz",
- "unicode": "1F1F8-1F1FF",
+ "flag_sz": {
+ "category": "flags",
+ "moji": "🇸🇿",
+ "unicodeVersion": "6.0",
"digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64"
},
- {
- "name": "flag_ta",
- "unicode": "1F1F9-1F1E6",
- "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
- },
- {
- "name": "ta",
- "unicode": "1F1F9-1F1E6",
+ "flag_ta": {
+ "category": "flags",
+ "moji": "🇹🇦",
+ "unicodeVersion": "6.0",
"digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0"
},
- {
- "name": "flag_tc",
- "unicode": "1F1F9-1F1E8",
- "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
- },
- {
- "name": "tc",
- "unicode": "1F1F9-1F1E8",
+ "flag_tc": {
+ "category": "flags",
+ "moji": "🇹🇨",
+ "unicodeVersion": "6.0",
"digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c"
},
- {
- "name": "flag_td",
- "unicode": "1F1F9-1F1E9",
+ "flag_td": {
+ "category": "flags",
+ "moji": "🇹🇩",
+ "unicodeVersion": "6.0",
"digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
},
- {
- "name": "td",
- "unicode": "1F1F9-1F1E9",
- "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db"
- },
- {
- "name": "flag_tf",
- "unicode": "1F1F9-1F1EB",
- "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
- },
- {
- "name": "tf",
- "unicode": "1F1F9-1F1EB",
+ "flag_tf": {
+ "category": "flags",
+ "moji": "🇹🇫",
+ "unicodeVersion": "6.0",
"digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435"
},
- {
- "name": "flag_tg",
- "unicode": "1F1F9-1F1EC",
- "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
- },
- {
- "name": "tg",
- "unicode": "1F1F9-1F1EC",
+ "flag_tg": {
+ "category": "flags",
+ "moji": "🇹🇬",
+ "unicodeVersion": "6.0",
"digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa"
},
- {
- "name": "flag_th",
- "unicode": "1F1F9-1F1ED",
- "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
- },
- {
- "name": "th",
- "unicode": "1F1F9-1F1ED",
+ "flag_th": {
+ "category": "flags",
+ "moji": "🇹🇭",
+ "unicodeVersion": "6.0",
"digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd"
},
- {
- "name": "flag_tj",
- "unicode": "1F1F9-1F1EF",
- "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
- },
- {
- "name": "tj",
- "unicode": "1F1F9-1F1EF",
+ "flag_tj": {
+ "category": "flags",
+ "moji": "🇹🇯",
+ "unicodeVersion": "6.0",
"digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d"
},
- {
- "name": "flag_tk",
- "unicode": "1F1F9-1F1F0",
- "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
- },
- {
- "name": "tk",
- "unicode": "1F1F9-1F1F0",
+ "flag_tk": {
+ "category": "flags",
+ "moji": "🇹🇰",
+ "unicodeVersion": "6.0",
"digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52"
},
- {
- "name": "flag_tl",
- "unicode": "1F1F9-1F1F1",
- "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
- },
- {
- "name": "tl",
- "unicode": "1F1F9-1F1F1",
+ "flag_tl": {
+ "category": "flags",
+ "moji": "🇹🇱",
+ "unicodeVersion": "6.0",
"digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473"
},
- {
- "name": "flag_tm",
- "unicode": "1F1F9-1F1F2",
- "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
- },
- {
- "name": "turkmenistan",
- "unicode": "1F1F9-1F1F2",
+ "flag_tm": {
+ "category": "flags",
+ "moji": "🇹🇲",
+ "unicodeVersion": "6.0",
"digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21"
},
- {
- "name": "flag_tn",
- "unicode": "1F1F9-1F1F3",
+ "flag_tn": {
+ "category": "flags",
+ "moji": "🇹🇳",
+ "unicodeVersion": "6.0",
"digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
},
- {
- "name": "tn",
- "unicode": "1F1F9-1F1F3",
- "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9"
- },
- {
- "name": "flag_to",
- "unicode": "1F1F9-1F1F4",
- "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
- },
- {
- "name": "to",
- "unicode": "1F1F9-1F1F4",
+ "flag_to": {
+ "category": "flags",
+ "moji": "🇹🇴",
+ "unicodeVersion": "6.0",
"digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723"
},
- {
- "name": "flag_tr",
- "unicode": "1F1F9-1F1F7",
- "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
- },
- {
- "name": "tr",
- "unicode": "1F1F9-1F1F7",
+ "flag_tr": {
+ "category": "flags",
+ "moji": "🇹🇷",
+ "unicodeVersion": "6.0",
"digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c"
},
- {
- "name": "flag_tt",
- "unicode": "1F1F9-1F1F9",
+ "flag_tt": {
+ "category": "flags",
+ "moji": "🇹🇹",
+ "unicodeVersion": "6.0",
"digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
},
- {
- "name": "tt",
- "unicode": "1F1F9-1F1F9",
- "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59"
- },
- {
- "name": "flag_tv",
- "unicode": "1F1F9-1F1FB",
- "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
- },
- {
- "name": "tuvalu",
- "unicode": "1F1F9-1F1FB",
+ "flag_tv": {
+ "category": "flags",
+ "moji": "🇹🇻",
+ "unicodeVersion": "6.0",
"digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc"
},
- {
- "name": "flag_tw",
- "unicode": "1F1F9-1F1FC",
+ "flag_tw": {
+ "category": "flags",
+ "moji": "🇹🇼",
+ "unicodeVersion": "6.0",
"digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
},
- {
- "name": "tw",
- "unicode": "1F1F9-1F1FC",
- "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c"
- },
- {
- "name": "flag_tz",
- "unicode": "1F1F9-1F1FF",
- "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
- },
- {
- "name": "tz",
- "unicode": "1F1F9-1F1FF",
+ "flag_tz": {
+ "category": "flags",
+ "moji": "🇹🇿",
+ "unicodeVersion": "6.0",
"digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4"
},
- {
- "name": "flag_ua",
- "unicode": "1F1FA-1F1E6",
+ "flag_ua": {
+ "category": "flags",
+ "moji": "🇺🇦",
+ "unicodeVersion": "6.0",
"digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
},
- {
- "name": "ua",
- "unicode": "1F1FA-1F1E6",
- "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30"
- },
- {
- "name": "flag_ug",
- "unicode": "1F1FA-1F1EC",
- "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
- },
- {
- "name": "ug",
- "unicode": "1F1FA-1F1EC",
+ "flag_ug": {
+ "category": "flags",
+ "moji": "🇺🇬",
+ "unicodeVersion": "6.0",
"digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c"
},
- {
- "name": "flag_um",
- "unicode": "1F1FA-1F1F2",
+ "flag_um": {
+ "category": "flags",
+ "moji": "🇺🇲",
+ "unicodeVersion": "6.0",
"digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
},
- {
- "name": "um",
- "unicode": "1F1FA-1F1F2",
- "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee"
- },
- {
- "name": "flag_us",
- "unicode": "1F1FA-1F1F8",
- "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
- },
- {
- "name": "us",
- "unicode": "1F1FA-1F1F8",
+ "flag_us": {
+ "category": "flags",
+ "moji": "🇺🇸",
+ "unicodeVersion": "6.0",
"digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63"
},
- {
- "name": "flag_uy",
- "unicode": "1F1FA-1F1FE",
+ "flag_uy": {
+ "category": "flags",
+ "moji": "🇺🇾",
+ "unicodeVersion": "6.0",
"digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
},
- {
- "name": "uy",
- "unicode": "1F1FA-1F1FE",
- "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7"
- },
- {
- "name": "flag_uz",
- "unicode": "1F1FA-1F1FF",
- "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
- },
- {
- "name": "uz",
- "unicode": "1F1FA-1F1FF",
+ "flag_uz": {
+ "category": "flags",
+ "moji": "🇺🇿",
+ "unicodeVersion": "6.0",
"digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c"
},
- {
- "name": "flag_va",
- "unicode": "1F1FB-1F1E6",
+ "flag_va": {
+ "category": "flags",
+ "moji": "🇻🇦",
+ "unicodeVersion": "6.0",
"digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
},
- {
- "name": "va",
- "unicode": "1F1FB-1F1E6",
- "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61"
- },
- {
- "name": "flag_vc",
- "unicode": "1F1FB-1F1E8",
- "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
- },
- {
- "name": "vc",
- "unicode": "1F1FB-1F1E8",
+ "flag_vc": {
+ "category": "flags",
+ "moji": "🇻🇨",
+ "unicodeVersion": "6.0",
"digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7"
},
- {
- "name": "flag_ve",
- "unicode": "1F1FB-1F1EA",
+ "flag_ve": {
+ "category": "flags",
+ "moji": "🇻🇪",
+ "unicodeVersion": "6.0",
"digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
},
- {
- "name": "ve",
- "unicode": "1F1FB-1F1EA",
- "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a"
- },
- {
- "name": "flag_vg",
- "unicode": "1F1FB-1F1EC",
- "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
- },
- {
- "name": "vg",
- "unicode": "1F1FB-1F1EC",
+ "flag_vg": {
+ "category": "flags",
+ "moji": "🇻🇬",
+ "unicodeVersion": "6.0",
"digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a"
},
- {
- "name": "flag_vi",
- "unicode": "1F1FB-1F1EE",
+ "flag_vi": {
+ "category": "flags",
+ "moji": "🇻🇮",
+ "unicodeVersion": "6.0",
"digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
},
- {
- "name": "vi",
- "unicode": "1F1FB-1F1EE",
- "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375"
- },
- {
- "name": "flag_vn",
- "unicode": "1F1FB-1F1F3",
+ "flag_vn": {
+ "category": "flags",
+ "moji": "🇻🇳",
+ "unicodeVersion": "6.0",
"digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
},
- {
- "name": "vn",
- "unicode": "1F1FB-1F1F3",
- "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5"
- },
- {
- "name": "flag_vu",
- "unicode": "1F1FB-1F1FA",
+ "flag_vu": {
+ "category": "flags",
+ "moji": "🇻🇺",
+ "unicodeVersion": "6.0",
"digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
},
- {
- "name": "vu",
- "unicode": "1F1FB-1F1FA",
- "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362"
- },
- {
- "name": "flag_wf",
- "unicode": "1F1FC-1F1EB",
- "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
- },
- {
- "name": "wf",
- "unicode": "1F1FC-1F1EB",
+ "flag_wf": {
+ "category": "flags",
+ "moji": "🇼🇫",
+ "unicodeVersion": "6.0",
"digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9"
},
- {
- "name": "flag_white",
- "unicode": "1F3F3",
+ "flag_white": {
+ "category": "objects",
+ "moji": "🏳",
+ "unicodeVersion": "6.0",
"digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
},
- {
- "name": "waving_white_flag",
- "unicode": "1F3F3",
- "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c"
- },
- {
- "name": "flag_ws",
- "unicode": "1F1FC-1F1F8",
- "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
- },
- {
- "name": "ws",
- "unicode": "1F1FC-1F1F8",
+ "flag_ws": {
+ "category": "flags",
+ "moji": "🇼🇸",
+ "unicodeVersion": "6.0",
"digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649"
},
- {
- "name": "flag_xk",
- "unicode": "1F1FD-1F1F0",
- "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
- },
- {
- "name": "xk",
- "unicode": "1F1FD-1F1F0",
+ "flag_xk": {
+ "category": "flags",
+ "moji": "🇽🇰",
+ "unicodeVersion": "6.0",
"digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469"
},
- {
- "name": "flag_ye",
- "unicode": "1F1FE-1F1EA",
- "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
- },
- {
- "name": "ye",
- "unicode": "1F1FE-1F1EA",
+ "flag_ye": {
+ "category": "flags",
+ "moji": "🇾🇪",
+ "unicodeVersion": "6.0",
"digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0"
},
- {
- "name": "flag_yt",
- "unicode": "1F1FE-1F1F9",
+ "flag_yt": {
+ "category": "flags",
+ "moji": "🇾🇹",
+ "unicodeVersion": "6.0",
"digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
},
- {
- "name": "yt",
- "unicode": "1F1FE-1F1F9",
- "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b"
- },
- {
- "name": "flag_za",
- "unicode": "1F1FF-1F1E6",
- "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
- },
- {
- "name": "za",
- "unicode": "1F1FF-1F1E6",
+ "flag_za": {
+ "category": "flags",
+ "moji": "🇿🇦",
+ "unicodeVersion": "6.0",
"digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce"
},
- {
- "name": "flag_zm",
- "unicode": "1F1FF-1F1F2",
- "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
- },
- {
- "name": "zm",
- "unicode": "1F1FF-1F1F2",
+ "flag_zm": {
+ "category": "flags",
+ "moji": "🇿🇲",
+ "unicodeVersion": "6.0",
"digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438"
},
- {
- "name": "flag_zw",
- "unicode": "1F1FF-1F1FC",
- "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
- },
- {
- "name": "zw",
- "unicode": "1F1FF-1F1FC",
+ "flag_zw": {
+ "category": "flags",
+ "moji": "🇿🇼",
+ "unicodeVersion": "6.0",
"digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825"
},
- {
- "name": "flags",
- "unicode": "1F38F",
+ "flags": {
+ "category": "objects",
+ "moji": "🎏",
+ "unicodeVersion": "6.0",
"digest": "f860aa4df587cf140c3e9735bbd101e9fd5a1bfcea42e420d85ac0a9877fa21d"
},
- {
- "name": "flashlight",
- "unicode": "1F526",
+ "flashlight": {
+ "category": "objects",
+ "moji": "🔦",
+ "unicodeVersion": "6.0",
"digest": "e929bbe76e0fd2dc5bd6476858a0bbc717fd21467710435d35d80efb38033d73"
},
- {
- "name": "fleur-de-lis",
- "unicode": "269C",
+ "fleur-de-lis": {
+ "category": "symbols",
+ "moji": "⚜",
+ "unicodeVersion": "4.1",
"digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3"
},
- {
- "name": "floppy_disk",
- "unicode": "1F4BE",
+ "floppy_disk": {
+ "category": "objects",
+ "moji": "💾",
+ "unicodeVersion": "6.0",
"digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0"
},
- {
- "name": "flower_playing_cards",
- "unicode": "1F3B4",
+ "flower_playing_cards": {
+ "category": "symbols",
+ "moji": "🎴",
+ "unicodeVersion": "6.0",
"digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869"
},
- {
- "name": "flushed",
- "unicode": "1F633",
+ "flushed": {
+ "category": "people",
+ "moji": "😳",
+ "unicodeVersion": "6.0",
"digest": "e759d46bab92af5494d78b6c712c06568759afe397e7828ca0a0de1e3eab0165"
},
- {
- "name": "fog",
- "unicode": "1F32B",
+ "fog": {
+ "category": "nature",
+ "moji": "🌫",
+ "unicodeVersion": "7.0",
"digest": "0cbd4733961d30fe0f40f95dd1f37254aebbef26f82dd18ad2000e799eb2898e"
},
- {
- "name": "foggy",
- "unicode": "1F301",
+ "foggy": {
+ "category": "travel",
+ "moji": "🌁",
+ "unicodeVersion": "6.0",
"digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca"
},
- {
- "name": "football",
- "unicode": "1F3C8",
+ "football": {
+ "category": "activity",
+ "moji": "🏈",
+ "unicodeVersion": "6.0",
"digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c"
},
- {
- "name": "footprints",
- "unicode": "1F463",
+ "footprints": {
+ "category": "people",
+ "moji": "👣",
+ "unicodeVersion": "6.0",
"digest": "85bbf2bc0ae8e6259d83a06f513600095d7fcfc44372670f5b2405d380b78811"
},
- {
- "name": "fork_and_knife",
- "unicode": "1F374",
+ "fork_and_knife": {
+ "category": "food",
+ "moji": "🍴",
+ "unicodeVersion": "6.0",
"digest": "f228accd36ddccb4ec636207c19d7185191ec79723b780a1bd5c3d00a4b1ef3b"
},
- {
- "name": "fork_knife_plate",
- "unicode": "1F37D",
- "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
- },
- {
- "name": "fork_and_knife_with_plate",
- "unicode": "1F37D",
+ "fork_knife_plate": {
+ "category": "food",
+ "moji": "🍽",
+ "unicodeVersion": "7.0",
"digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e"
},
- {
- "name": "fountain",
- "unicode": "26F2",
+ "fountain": {
+ "category": "travel",
+ "moji": "⛲",
+ "unicodeVersion": "5.2",
"digest": "87043f9256e1d4615159307fcfd21bf6ae2aba0bada7de2bd50d7d6f2ab82395"
},
- {
- "name": "four",
- "unicode": "0034-20E3",
+ "four": {
+ "category": "symbols",
+ "moji": "4️⃣",
+ "unicodeVersion": "3.0",
"digest": "c2c82a966bbb599aae557d930a4fc42604f2081aa45528872f5caf4942ee79d9"
},
- {
- "name": "four_leaf_clover",
- "unicode": "1F340",
+ "four_leaf_clover": {
+ "category": "nature",
+ "moji": "🍀",
+ "unicodeVersion": "6.0",
"digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8"
},
- {
- "name": "fox",
- "unicode": "1F98A",
- "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
- },
- {
- "name": "fox_face",
- "unicode": "1F98A",
+ "fox": {
+ "category": "nature",
+ "moji": "🦊",
+ "unicodeVersion": "9.0",
"digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
},
- {
- "name": "frame_photo",
- "unicode": "1F5BC",
- "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
- },
- {
- "name": "frame_with_picture",
- "unicode": "1F5BC",
+ "frame_photo": {
+ "category": "objects",
+ "moji": "🖼",
+ "unicodeVersion": "7.0",
"digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
},
- {
- "name": "free",
- "unicode": "1F193",
+ "free": {
+ "category": "symbols",
+ "moji": "🆓",
+ "unicodeVersion": "6.0",
"digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa"
},
- {
- "name": "french_bread",
- "unicode": "1F956",
- "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
- },
- {
- "name": "baguette_bread",
- "unicode": "1F956",
+ "french_bread": {
+ "category": "food",
+ "moji": "🥖",
+ "unicodeVersion": "9.0",
"digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
},
- {
- "name": "fried_shrimp",
- "unicode": "1F364",
+ "fried_shrimp": {
+ "category": "food",
+ "moji": "🍤",
+ "unicodeVersion": "6.0",
"digest": "0792bdc4484852de970c8f43bc3a1a339dc0e48090ec77d6de97cbfcdd17f9e1"
},
- {
- "name": "fries",
- "unicode": "1F35F",
+ "fries": {
+ "category": "food",
+ "moji": "🍟",
+ "unicodeVersion": "6.0",
"digest": "47915aea67251d358d91a0e4dc3dcc347155336007d6b931a192be72a743b4e9"
},
- {
- "name": "frog",
- "unicode": "1F438",
+ "frog": {
+ "category": "nature",
+ "moji": "🐸",
+ "unicodeVersion": "6.0",
"digest": "d024b2ce771df64040534fb0906737d18b562bc3578dee62c2f25ec03c7caffd"
},
- {
- "name": "frowning",
- "unicode": "1F626",
- "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
- },
- {
- "name": "anguished",
- "unicode": "1F626",
+ "frowning": {
+ "category": "people",
+ "moji": "😦",
+ "unicodeVersion": "6.1",
"digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44"
},
- {
- "name": "frowning2",
- "unicode": "2639",
- "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
- },
- {
- "name": "white_frowning_face",
- "unicode": "2639",
+ "frowning2": {
+ "category": "people",
+ "moji": "☹",
+ "unicodeVersion": "1.1",
"digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf"
},
- {
- "name": "fuelpump",
- "unicode": "26FD",
+ "fuelpump": {
+ "category": "travel",
+ "moji": "⛽",
+ "unicodeVersion": "5.2",
"digest": "105e736469f19911b8bab4ab6d29f949ded4b061b54e3dd763726577d6453095"
},
- {
- "name": "full_moon",
- "unicode": "1F315",
+ "full_moon": {
+ "category": "nature",
+ "moji": "🌕",
+ "unicodeVersion": "6.0",
"digest": "aaa87f4676a5aaa29c1b721a3b582e89db6c1f35a25c52e4b480bd193ef39c43"
},
- {
- "name": "full_moon_with_face",
- "unicode": "1F31D",
+ "full_moon_with_face": {
+ "category": "nature",
+ "moji": "🌝",
+ "unicodeVersion": "6.0",
"digest": "05c4b9c339fcdf81ae67027641522baa99c370d87873ff4c8133b8349e627e33"
},
- {
- "name": "game_die",
- "unicode": "1F3B2",
+ "game_die": {
+ "category": "activity",
+ "moji": "🎲",
+ "unicodeVersion": "6.0",
"digest": "00d19ce8e21dba2cdfeb18709fa8741f3af9d6207f81d5657b68e05e64f105a8"
},
- {
- "name": "gear",
- "unicode": "2699",
+ "gear": {
+ "category": "objects",
+ "moji": "⚙",
+ "unicodeVersion": "4.1",
"digest": "c5ba354c0f7a36dce95477091984e352ecc59af8c9f26a94ad8e296dc042b9de"
},
- {
- "name": "gem",
- "unicode": "1F48E",
+ "gem": {
+ "category": "objects",
+ "moji": "💎",
+ "unicodeVersion": "6.0",
"digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
},
- {
- "name": "gemini",
- "unicode": "264A",
+ "gemini": {
+ "category": "symbols",
+ "moji": "♊",
+ "unicodeVersion": "1.1",
"digest": "278239c598d490a110f1f3f52fc3b85259be8e76034b38228ef3f68d7ddd8cdd"
},
- {
- "name": "ghost",
- "unicode": "1F47B",
+ "ghost": {
+ "category": "people",
+ "moji": "👻",
+ "unicodeVersion": "6.0",
"digest": "80d528fcf8ef9198631527547e43a608a4332a799f9e5550b8318dec67c9c4d2"
},
- {
- "name": "gift",
- "unicode": "1F381",
+ "gift": {
+ "category": "objects",
+ "moji": "🎁",
+ "unicodeVersion": "6.0",
"digest": "4061a84a59f0300473299678c43e533341eb965db09597fffc6e221fd7b77376"
},
- {
- "name": "gift_heart",
- "unicode": "1F49D",
+ "gift_heart": {
+ "category": "symbols",
+ "moji": "💝",
+ "unicodeVersion": "6.0",
"digest": "5420199b515b9b32c964a3c19d87e07461639e3068a939dae26c6436335c0cee"
},
- {
- "name": "girl",
- "unicode": "1F467",
+ "girl": {
+ "category": "people",
+ "moji": "👧",
+ "unicodeVersion": "6.0",
"digest": "8d2d0b72a91e6e44921b71030ffc4c89c0f50f1364787784afe1e7e568cf1bc6"
},
- {
- "name": "girl_tone1",
- "unicode": "1F467-1F3FB",
+ "girl_tone1": {
+ "category": "people",
+ "moji": "👧🏻",
+ "unicodeVersion": "8.0",
"digest": "bda12a6b38994a578ee65166bbdd93ea04df4101697b52ed236de8d687df09de"
},
- {
- "name": "girl_tone2",
- "unicode": "1F467-1F3FC",
+ "girl_tone2": {
+ "category": "people",
+ "moji": "👧🏼",
+ "unicodeVersion": "8.0",
"digest": "de7a0925c30b7181a289f71b1a849c1b7751ee8c104e8f2029bd9c2fe3f91c64"
},
- {
- "name": "girl_tone3",
- "unicode": "1F467-1F3FD",
+ "girl_tone3": {
+ "category": "people",
+ "moji": "👧🏽",
+ "unicodeVersion": "8.0",
"digest": "e41272816db0e642d003dce7cb262e1593a592251f46729f7830f4515149e1f2"
},
- {
- "name": "girl_tone4",
- "unicode": "1F467-1F3FE",
+ "girl_tone4": {
+ "category": "people",
+ "moji": "👧🏾",
+ "unicodeVersion": "8.0",
"digest": "8d6a4513ecbf08408c0ecc5336767777a2216f7a19437faf9e51f65101822469"
},
- {
- "name": "girl_tone5",
- "unicode": "1F467-1F3FF",
+ "girl_tone5": {
+ "category": "people",
+ "moji": "👧🏿",
+ "unicodeVersion": "8.0",
"digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d"
},
- {
- "name": "globe_with_meridians",
- "unicode": "1F310",
+ "globe_with_meridians": {
+ "category": "symbols",
+ "moji": "🌐",
+ "unicodeVersion": "6.0",
"digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75"
},
- {
- "name": "goal",
- "unicode": "1F945",
- "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
- },
- {
- "name": "goal_net",
- "unicode": "1F945",
+ "goal": {
+ "category": "activity",
+ "moji": "🥅",
+ "unicodeVersion": "9.0",
"digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
},
- {
- "name": "goat",
- "unicode": "1F410",
+ "goat": {
+ "category": "nature",
+ "moji": "🐐",
+ "unicodeVersion": "6.0",
"digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8"
},
- {
- "name": "golf",
- "unicode": "26F3",
+ "golf": {
+ "category": "activity",
+ "moji": "⛳",
+ "unicodeVersion": "5.2",
"digest": "eed79364754eec97855e3c7b584f347ae139d9ddb4eb7fb66c00867610b8f1c1"
},
- {
- "name": "golfer",
- "unicode": "1F3CC",
+ "golfer": {
+ "category": "activity",
+ "moji": "🏌",
+ "unicodeVersion": "7.0",
"digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344"
},
- {
- "name": "gorilla",
- "unicode": "1F98D",
+ "gorilla": {
+ "category": "nature",
+ "moji": "🦍",
+ "unicodeVersion": "9.0",
"digest": "4a564dc14f8ae5450d094f6410ec7f099a7f07dc5254b6395f44a35527bdb4b7"
},
- {
- "name": "grapes",
- "unicode": "1F347",
+ "grapes": {
+ "category": "food",
+ "moji": "🍇",
+ "unicodeVersion": "6.0",
"digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50"
},
- {
- "name": "green_apple",
- "unicode": "1F34F",
+ "green_apple": {
+ "category": "food",
+ "moji": "🍏",
+ "unicodeVersion": "6.0",
"digest": "457490e9b2b20894f50768262d63f1021717079da104d4847076b3fa779e9a21"
},
- {
- "name": "green_book",
- "unicode": "1F4D7",
+ "green_book": {
+ "category": "objects",
+ "moji": "📗",
+ "unicodeVersion": "6.0",
"digest": "370f635b200efe5e4a9f17da58bd22500e258e61d17795cef375f19c9a45468f"
},
- {
- "name": "green_heart",
- "unicode": "1F49A",
+ "green_heart": {
+ "category": "symbols",
+ "moji": "💚",
+ "unicodeVersion": "6.0",
"digest": "f71e30416d9019873f2ed38ef375c48386424ff60b5a07b89b15dc9e0a3970f9"
},
- {
- "name": "grey_exclamation",
- "unicode": "2755",
+ "grey_exclamation": {
+ "category": "symbols",
+ "moji": "❕",
+ "unicodeVersion": "6.0",
"digest": "2fa1d356e12c17cc4025e43afb6c3070385f677102a35223302fda46c47a9b03"
},
- {
- "name": "grey_question",
- "unicode": "2754",
+ "grey_question": {
+ "category": "symbols",
+ "moji": "❔",
+ "unicodeVersion": "6.0",
"digest": "e1035bcbf0f66d238ef478ba451f5cf2c51627fbf101ed03bad3b2bf38db8aa2"
},
- {
- "name": "grimacing",
- "unicode": "1F62C",
+ "grimacing": {
+ "category": "people",
+ "moji": "😬",
+ "unicodeVersion": "6.1",
"digest": "2cedad13b8b2a1d4385ca6fa88a251eb7757a4c65dd6d362267864a01247846b"
},
- {
- "name": "grin",
- "unicode": "1F601",
+ "grin": {
+ "category": "people",
+ "moji": "😁",
+ "unicodeVersion": "6.0",
"digest": "634b2f37e32e57ed6edc7f371993a92e34137dd21ba393de5227cfbbe2422815"
},
- {
- "name": "grinning",
- "unicode": "1F600",
+ "grinning": {
+ "category": "people",
+ "moji": "😀",
+ "unicodeVersion": "6.1",
"digest": "cef76aa41771db9fd1d6bd9b4233c22c1fb1931494af54cab29e6347ed9b678d"
},
- {
- "name": "guardsman",
- "unicode": "1F482",
+ "guardsman": {
+ "category": "people",
+ "moji": "💂",
+ "unicodeVersion": "6.0",
"digest": "17bc7fad6b8c8dbd015bb709380d129f8b8e1e971062d15e6ab0b2e63e500564"
},
- {
- "name": "guardsman_tone1",
- "unicode": "1F482-1F3FB",
+ "guardsman_tone1": {
+ "category": "people",
+ "moji": "💂🏻",
+ "unicodeVersion": "8.0",
"digest": "c531ecb101bdf9ce1db18e1567882e6db927410237100b0a2492a1401860246e"
},
- {
- "name": "guardsman_tone2",
- "unicode": "1F482-1F3FC",
+ "guardsman_tone2": {
+ "category": "people",
+ "moji": "💂🏼",
+ "unicodeVersion": "8.0",
"digest": "602168c5204af0f1de8b4aa5863b192ef20c19d263999377aa5eb60f98311732"
},
- {
- "name": "guardsman_tone3",
- "unicode": "1F482-1F3FD",
+ "guardsman_tone3": {
+ "category": "people",
+ "moji": "💂🏽",
+ "unicodeVersion": "8.0",
"digest": "d0a85de46dd02c7bd6cb14bff0f22d2db9083d4b171a8806c83363b49f3dd9ef"
},
- {
- "name": "guardsman_tone4",
- "unicode": "1F482-1F3FE",
+ "guardsman_tone4": {
+ "category": "people",
+ "moji": "💂🏾",
+ "unicodeVersion": "8.0",
"digest": "1c9d4d72b6b50bdac8271613b6d2a38340ec2067bc344e8ee2a3c863fd5c23a1"
},
- {
- "name": "guardsman_tone5",
- "unicode": "1F482-1F3FF",
+ "guardsman_tone5": {
+ "category": "people",
+ "moji": "💂🏿",
+ "unicodeVersion": "8.0",
"digest": "9899a796d01842e495d716fbe737a16d85724f7d3e23f50807ec2bc70f057318"
},
- {
- "name": "guitar",
- "unicode": "1F3B8",
+ "guitar": {
+ "category": "activity",
+ "moji": "🎸",
+ "unicodeVersion": "6.0",
"digest": "a1027ceae4dd3ea270740587c9d373329e5677e375c9e00af6ae3275e0b67500"
},
- {
- "name": "gun",
- "unicode": "1F52B",
+ "gun": {
+ "category": "objects",
+ "moji": "🔫",
+ "unicodeVersion": "6.0",
"digest": "fc12b577df2283e7b336f23774f9cfe5b79f1d26ddd28a64a560519b28d94ca5"
},
- {
- "name": "haircut",
- "unicode": "1F487",
+ "haircut": {
+ "category": "people",
+ "moji": "💇",
+ "unicodeVersion": "6.0",
"digest": "b243a04f5ca889accd45e7abe095ac5caa92274ed95103f5966a36b415fff412"
},
- {
- "name": "haircut_tone1",
- "unicode": "1F487-1F3FB",
+ "haircut_tone1": {
+ "category": "people",
+ "moji": "💇🏻",
+ "unicodeVersion": "8.0",
"digest": "a58d0cff1427b80dfd7a9ea5267b4a181e9faaac6a51a0165db522f668b4cf91"
},
- {
- "name": "haircut_tone2",
- "unicode": "1F487-1F3FC",
+ "haircut_tone2": {
+ "category": "people",
+ "moji": "💇🏼",
+ "unicodeVersion": "8.0",
"digest": "675083ff40001405f8de99268477d50dd8594ff6ca40ddfd442dd42ad76e8216"
},
- {
- "name": "haircut_tone3",
- "unicode": "1F487-1F3FD",
+ "haircut_tone3": {
+ "category": "people",
+ "moji": "💇🏽",
+ "unicodeVersion": "8.0",
"digest": "70d7581e49c315a3771dd61a3713229886db32aaaeb3af078a69cc042f809150"
},
- {
- "name": "haircut_tone4",
- "unicode": "1F487-1F3FE",
+ "haircut_tone4": {
+ "category": "people",
+ "moji": "💇🏾",
+ "unicodeVersion": "8.0",
"digest": "ec5e3e909eb3bc375ef9cc0fe0e0f90b33f44f273ada91ccf415bbc43b8ffbfc"
},
- {
- "name": "haircut_tone5",
- "unicode": "1F487-1F3FF",
+ "haircut_tone5": {
+ "category": "people",
+ "moji": "💇🏿",
+ "unicodeVersion": "8.0",
"digest": "7c89739ee458546a808fded7f96d9354c47a76883ebb262d5f5abeafd021260e"
},
- {
- "name": "hamburger",
- "unicode": "1F354",
+ "hamburger": {
+ "category": "food",
+ "moji": "🍔",
+ "unicodeVersion": "6.0",
"digest": "48204235238bd89d3a69f319f65135102f3d6b181eec241d4d86b302bbffa9bf"
},
- {
- "name": "hammer",
- "unicode": "1F528",
+ "hammer": {
+ "category": "objects",
+ "moji": "🔨",
+ "unicodeVersion": "6.0",
"digest": "d0e7830539d935fcd82820c4e0c1d724f0756dfc83a51171fe0f4b36b69fac42"
},
- {
- "name": "hammer_pick",
- "unicode": "2692",
+ "hammer_pick": {
+ "category": "objects",
+ "moji": "⚒",
+ "unicodeVersion": "4.1",
"digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
},
- {
- "name": "hammer_and_pick",
- "unicode": "2692",
- "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142"
- },
- {
- "name": "hamster",
- "unicode": "1F439",
+ "hamster": {
+ "category": "nature",
+ "moji": "🐹",
+ "unicodeVersion": "6.0",
"digest": "a7e7582e8b1bccd5b7df27ccb05e353a3f0e39bdeb40877732706b9d74a70de1"
},
- {
- "name": "hand_splayed",
- "unicode": "1F590",
- "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
- },
- {
- "name": "raised_hand_with_fingers_splayed",
- "unicode": "1F590",
+ "hand_splayed": {
+ "category": "people",
+ "moji": "🖐",
+ "unicodeVersion": "7.0",
"digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
},
- {
- "name": "hand_splayed_tone1",
- "unicode": "1F590-1F3FB",
+ "hand_splayed_tone1": {
+ "category": "people",
+ "moji": "🖐🏻",
+ "unicodeVersion": "8.0",
"digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
},
- {
- "name": "raised_hand_with_fingers_splayed_tone1",
- "unicode": "1F590-1F3FB",
- "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
- },
- {
- "name": "hand_splayed_tone2",
- "unicode": "1F590-1F3FC",
- "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
- },
- {
- "name": "raised_hand_with_fingers_splayed_tone2",
- "unicode": "1F590-1F3FC",
+ "hand_splayed_tone2": {
+ "category": "people",
+ "moji": "🖐🏼",
+ "unicodeVersion": "8.0",
"digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf"
},
- {
- "name": "hand_splayed_tone3",
- "unicode": "1F590-1F3FD",
+ "hand_splayed_tone3": {
+ "category": "people",
+ "moji": "🖐🏽",
+ "unicodeVersion": "8.0",
"digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
},
- {
- "name": "raised_hand_with_fingers_splayed_tone3",
- "unicode": "1F590-1F3FD",
- "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425"
- },
- {
- "name": "hand_splayed_tone4",
- "unicode": "1F590-1F3FE",
- "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
- },
- {
- "name": "raised_hand_with_fingers_splayed_tone4",
- "unicode": "1F590-1F3FE",
+ "hand_splayed_tone4": {
+ "category": "people",
+ "moji": "🖐🏾",
+ "unicodeVersion": "8.0",
"digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481"
},
- {
- "name": "hand_splayed_tone5",
- "unicode": "1F590-1F3FF",
+ "hand_splayed_tone5": {
+ "category": "people",
+ "moji": "🖐🏿",
+ "unicodeVersion": "8.0",
"digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
},
- {
- "name": "raised_hand_with_fingers_splayed_tone5",
- "unicode": "1F590-1F3FF",
- "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
- },
- {
- "name": "handbag",
- "unicode": "1F45C",
+ "handbag": {
+ "category": "people",
+ "moji": "👜",
+ "unicodeVersion": "6.0",
"digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45"
},
- {
- "name": "handball",
- "unicode": "1F93E",
+ "handball": {
+ "category": "activity",
+ "moji": "🤾",
+ "unicodeVersion": "9.0",
"digest": "94ceb28024eb3259d8b137cafd7438773e717fbc04f5da810f85e43ca0fa9e00"
},
- {
- "name": "handball_tone1",
- "unicode": "1F93E-1F3FB",
+ "handball_tone1": {
+ "category": "activity",
+ "moji": "🤾🏻",
+ "unicodeVersion": "9.0",
"digest": "8bec4de0d05c80e335e44d65598d186ca92696977353c9fd9c2a5efa122cb842"
},
- {
- "name": "handball_tone2",
- "unicode": "1F93E-1F3FC",
+ "handball_tone2": {
+ "category": "activity",
+ "moji": "🤾🏼",
+ "unicodeVersion": "9.0",
"digest": "2ff4131e1e2f089b315d8e176c9348877c26c2bd03706fb75d41bc61bc99bf93"
},
- {
- "name": "handball_tone3",
- "unicode": "1F93E-1F3FD",
+ "handball_tone3": {
+ "category": "activity",
+ "moji": "🤾🏽",
+ "unicodeVersion": "9.0",
"digest": "224a71f94dd37d3729325d11412334667a81422e21f6d7c008730ff350f51a80"
},
- {
- "name": "handball_tone4",
- "unicode": "1F93E-1F3FE",
+ "handball_tone4": {
+ "category": "activity",
+ "moji": "🤾🏾",
+ "unicodeVersion": "9.0",
"digest": "a5f7a9db790565981bad2d0d9e09554c8c509a8179b4705a418300d58a7894b4"
},
- {
- "name": "handball_tone5",
- "unicode": "1F93E-1F3FF",
+ "handball_tone5": {
+ "category": "activity",
+ "moji": "🤾🏿",
+ "unicodeVersion": "9.0",
"digest": "00404572d4683f2e8e8a494aa733e96fbec1723634d0a8cb8d75f2829a789d27"
},
- {
- "name": "handshake",
- "unicode": "1F91D",
+ "handshake": {
+ "category": "people",
+ "moji": "🤝",
+ "unicodeVersion": "9.0",
"digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
},
- {
- "name": "shaking_hands",
- "unicode": "1F91D",
- "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
- },
- {
- "name": "handshake_tone1",
- "unicode": "1F91D-1F3FB",
+ "handshake_tone1": {
+ "category": "people",
+ "moji": "🤝🏻",
+ "unicodeVersion": "9.0",
"digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
},
- {
- "name": "shaking_hands_tone1",
- "unicode": "1F91D-1F3FB",
- "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
- },
- {
- "name": "handshake_tone2",
- "unicode": "1F91D-1F3FC",
- "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
- },
- {
- "name": "shaking_hands_tone2",
- "unicode": "1F91D-1F3FC",
+ "handshake_tone2": {
+ "category": "people",
+ "moji": "🤝🏼",
+ "unicodeVersion": "9.0",
"digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
},
- {
- "name": "handshake_tone3",
- "unicode": "1F91D-1F3FD",
+ "handshake_tone3": {
+ "category": "people",
+ "moji": "🤝🏽",
+ "unicodeVersion": "9.0",
"digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
},
- {
- "name": "shaking_hands_tone3",
- "unicode": "1F91D-1F3FD",
- "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
- },
- {
- "name": "handshake_tone4",
- "unicode": "1F91D-1F3FE",
- "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
- },
- {
- "name": "shaking_hands_tone4",
- "unicode": "1F91D-1F3FE",
+ "handshake_tone4": {
+ "category": "people",
+ "moji": "🤝🏾",
+ "unicodeVersion": "9.0",
"digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
},
- {
- "name": "handshake_tone5",
- "unicode": "1F91D-1F3FF",
- "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
- },
- {
- "name": "shaking_hands_tone5",
- "unicode": "1F91D-1F3FF",
+ "handshake_tone5": {
+ "category": "people",
+ "moji": "🤝🏿",
+ "unicodeVersion": "9.0",
"digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
},
- {
- "name": "hash",
- "unicode": "0023-20E3",
+ "hash": {
+ "category": "symbols",
+ "moji": "#⃣",
+ "unicodeVersion": "3.0",
"digest": "01c8b577953010bff0c20f797c2c96ab5d98d4e6ac179c4895a78f34ea904655"
},
- {
- "name": "hatched_chick",
- "unicode": "1F425",
+ "hatched_chick": {
+ "category": "nature",
+ "moji": "🐥",
+ "unicodeVersion": "6.0",
"digest": "006571b9e9e839ec9fcb1a911b935c8ca71eb8bcdce9775bee6a2a4c7c927277"
},
- {
- "name": "hatching_chick",
- "unicode": "1F423",
+ "hatching_chick": {
+ "category": "nature",
+ "moji": "🐣",
+ "unicodeVersion": "6.0",
"digest": "fd7f69fa186407f80de59dec5116e318325a5743ee0e8bba1db541f1e57e7f74"
},
- {
- "name": "head_bandage",
- "unicode": "1F915",
- "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
- },
- {
- "name": "face_with_head_bandage",
- "unicode": "1F915",
+ "head_bandage": {
+ "category": "people",
+ "moji": "🤕",
+ "unicodeVersion": "8.0",
"digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72"
},
- {
- "name": "headphones",
- "unicode": "1F3A7",
+ "headphones": {
+ "category": "activity",
+ "moji": "🎧",
+ "unicodeVersion": "6.0",
"digest": "34f9d5598158d5d6f978a5ea5c5aa9948bb2990625565a3afad7710f864fbe2f"
},
- {
- "name": "hear_no_evil",
- "unicode": "1F649",
+ "hear_no_evil": {
+ "category": "nature",
+ "moji": "🙉",
+ "unicodeVersion": "6.0",
"digest": "53b030b6d6f4ed1a734fa7d48b46f42eb1b2b01653202c1838b742082f08c4bf"
},
- {
- "name": "heart",
- "unicode": "2764",
+ "heart": {
+ "category": "symbols",
+ "moji": "❤",
+ "unicodeVersion": "1.1",
"digest": "92be652ec3e50c6e7393440b5d52b88a367f98a28dffe12660095ed3253aa6c0"
},
- {
- "name": "heart_decoration",
- "unicode": "1F49F",
+ "heart_decoration": {
+ "category": "symbols",
+ "moji": "💟",
+ "unicodeVersion": "6.0",
"digest": "6ec5bbf3aa75c6f43eb3dc05e9204366936e8b6b4219310bacdc2fc45f51e245"
},
- {
- "name": "heart_exclamation",
- "unicode": "2763",
+ "heart_exclamation": {
+ "category": "symbols",
+ "moji": "❣",
+ "unicodeVersion": "1.1",
"digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
},
- {
- "name": "heavy_heart_exclamation_mark_ornament",
- "unicode": "2763",
- "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6"
- },
- {
- "name": "heart_eyes",
- "unicode": "1F60D",
+ "heart_eyes": {
+ "category": "people",
+ "moji": "😍",
+ "unicodeVersion": "6.0",
"digest": "0eff616517a6252ec89d47d9b4ad85589bcf2bdc7f490578934350acb84b2fcc"
},
- {
- "name": "heart_eyes_cat",
- "unicode": "1F63B",
+ "heart_eyes_cat": {
+ "category": "people",
+ "moji": "😻",
+ "unicodeVersion": "6.0",
"digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6"
},
- {
- "name": "heartbeat",
- "unicode": "1F493",
+ "heartbeat": {
+ "category": "symbols",
+ "moji": "💓",
+ "unicodeVersion": "6.0",
"digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe"
},
- {
- "name": "heartpulse",
- "unicode": "1F497",
+ "heartpulse": {
+ "category": "symbols",
+ "moji": "💗",
+ "unicodeVersion": "6.0",
"digest": "281d8aebfea37db5b7fe82d9115be167006881fe29ab64a5b09ac92ac27a2309"
},
- {
- "name": "hearts",
- "unicode": "2665",
+ "hearts": {
+ "category": "symbols",
+ "moji": "♥",
+ "unicodeVersion": "1.1",
"digest": "271429d12c40be921897005b7bdd08f9518960af1e1e6f56bb0060f1f183651e"
},
- {
- "name": "heavy_check_mark",
- "unicode": "2714",
+ "heavy_check_mark": {
+ "category": "symbols",
+ "moji": "✔",
+ "unicodeVersion": "1.1",
"digest": "e347728e1290eb9e7b0742d628e2fd124fc049e0774f8a6ddf8e5286e7318718"
},
- {
- "name": "heavy_division_sign",
- "unicode": "2797",
+ "heavy_division_sign": {
+ "category": "symbols",
+ "moji": "➗",
+ "unicodeVersion": "6.0",
"digest": "c1e8c40f0788f140b1c5fcb81ed9b5ce1bcfa5988bb8140ed2808e9cb7e0d651"
},
- {
- "name": "heavy_dollar_sign",
- "unicode": "1F4B2",
+ "heavy_dollar_sign": {
+ "category": "symbols",
+ "moji": "💲",
+ "unicodeVersion": "6.0",
"digest": "7cdeef38348654b93d566e01a48973281cb404a63d0b75b3bad51032887f3f55"
},
- {
- "name": "heavy_minus_sign",
- "unicode": "2796",
+ "heavy_minus_sign": {
+ "category": "symbols",
+ "moji": "➖",
+ "unicodeVersion": "6.0",
"digest": "e5335cc6b22abdce49a6127c34269b65a4a6643ddd3253d9baac425089143e7d"
},
- {
- "name": "heavy_multiplication_x",
- "unicode": "2716",
+ "heavy_multiplication_x": {
+ "category": "symbols",
+ "moji": "✖",
+ "unicodeVersion": "1.1",
"digest": "64bbe9e9716a922e405d2f6d3b6d803863a53fac80ff8cd775899971046cb1ca"
},
- {
- "name": "heavy_plus_sign",
- "unicode": "2795",
+ "heavy_plus_sign": {
+ "category": "symbols",
+ "moji": "➕",
+ "unicodeVersion": "6.0",
"digest": "d0d8ade2020ceb252205180b85c66e665856e6cb505518d395b9913b0b24b746"
},
- {
- "name": "helicopter",
- "unicode": "1F681",
+ "helicopter": {
+ "category": "travel",
+ "moji": "🚁",
+ "unicodeVersion": "6.0",
"digest": "4bd6fd13650fbe3a19cfffeffe6c21b1cda74bd6af64c5dc5999185e35444bc3"
},
- {
- "name": "helmet_with_cross",
- "unicode": "26D1",
- "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
- },
- {
- "name": "helmet_with_white_cross",
- "unicode": "26D1",
+ "helmet_with_cross": {
+ "category": "people",
+ "moji": "⛑",
+ "unicodeVersion": "5.2",
"digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77"
},
- {
- "name": "herb",
- "unicode": "1F33F",
+ "herb": {
+ "category": "nature",
+ "moji": "🌿",
+ "unicodeVersion": "6.0",
"digest": "9fe8ed65515ede59d0926dcf98f14e2498785e1965610aa0dd56eca9b4bedad9"
},
- {
- "name": "hibiscus",
- "unicode": "1F33A",
+ "hibiscus": {
+ "category": "nature",
+ "moji": "🌺",
+ "unicodeVersion": "6.0",
"digest": "c442e8eacbd8727bd154bd39692a9a2a03ea2f674b9670ad8361f78a038afe49"
},
- {
- "name": "high_brightness",
- "unicode": "1F506",
+ "high_brightness": {
+ "category": "symbols",
+ "moji": "🔆",
+ "unicodeVersion": "6.0",
"digest": "35ced42426dcfd5214c2c6c577dce84bb708156433945e6b6adaff7ea530cc57"
},
- {
- "name": "high_heel",
- "unicode": "1F460",
+ "high_heel": {
+ "category": "people",
+ "moji": "👠",
+ "unicodeVersion": "6.0",
"digest": "1e7c7aba50eb1d02cf1d9aa372caca741a6005cf47f68dfa75b7310c3cb18f05"
},
- {
- "name": "hockey",
- "unicode": "1F3D2",
+ "hockey": {
+ "category": "activity",
+ "moji": "🏒",
+ "unicodeVersion": "8.0",
"digest": "2d00fb17baa617e799db8e9b1771cc365bb4545c7633df0123e66e1a6e2ed25d"
},
- {
- "name": "hole",
- "unicode": "1F573",
+ "hole": {
+ "category": "objects",
+ "moji": "🕳",
+ "unicodeVersion": "7.0",
"digest": "8b5539f6f24f09d5d68ffd56be5aa2a8a2f753a8dfbf64892fb02c8f2703e920"
},
- {
- "name": "homes",
- "unicode": "1F3D8",
- "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
- },
- {
- "name": "house_buildings",
- "unicode": "1F3D8",
+ "homes": {
+ "category": "travel",
+ "moji": "🏘",
+ "unicodeVersion": "7.0",
"digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f"
},
- {
- "name": "honey_pot",
- "unicode": "1F36F",
+ "honey_pot": {
+ "category": "food",
+ "moji": "🍯",
+ "unicodeVersion": "6.0",
"digest": "f6eec8c32fbd1b461446dc6c5d5031c43e6ee9685dc9b1ea1b839114e48c4eee"
},
- {
- "name": "horse",
- "unicode": "1F434",
+ "horse": {
+ "category": "nature",
+ "moji": "🐴",
+ "unicodeVersion": "6.0",
"digest": "e377649a9549835770a2a721a92570f699255f88efa646029638eb8ec5f10e3d"
},
- {
- "name": "horse_racing",
- "unicode": "1F3C7",
+ "horse_racing": {
+ "category": "activity",
+ "moji": "🏇",
+ "unicodeVersion": "6.0",
"digest": "3b98e94e9c028ad85b9a750cc61db5ee3ac23cf5ad9243ea3e996b1f772bad54"
},
- {
- "name": "horse_racing_tone1",
- "unicode": "1F3C7-1F3FB",
+ "horse_racing_tone1": {
+ "category": "activity",
+ "moji": "🏇🏻",
+ "unicodeVersion": "8.0",
"digest": "382d8e4502ed34fc1bbf1779ce483bc2e22b83f89c91746c11a5d7aea656d446"
},
- {
- "name": "horse_racing_tone2",
- "unicode": "1F3C7-1F3FC",
+ "horse_racing_tone2": {
+ "category": "activity",
+ "moji": "🏇🏼",
+ "unicodeVersion": "8.0",
"digest": "198df9973b492ea63e5cfc210dd9591750ccce04a6380adc1dc5b4cb0462a8cd"
},
- {
- "name": "horse_racing_tone3",
- "unicode": "1F3C7-1F3FD",
+ "horse_racing_tone3": {
+ "category": "activity",
+ "moji": "🏇🏽",
+ "unicodeVersion": "8.0",
"digest": "a67f95fc92c366750ebad3c4db92982893d67a5ed78163c8cc809ac40d2ab9a3"
},
- {
- "name": "horse_racing_tone4",
- "unicode": "1F3C7-1F3FE",
+ "horse_racing_tone4": {
+ "category": "activity",
+ "moji": "🏇🏾",
+ "unicodeVersion": "8.0",
"digest": "986b1706c4a3395b58a8ae3b7609ffdd4424dfefcbf26c88c8085f4f6379734e"
},
- {
- "name": "horse_racing_tone5",
- "unicode": "1F3C7-1F3FF",
+ "horse_racing_tone5": {
+ "category": "activity",
+ "moji": "🏇🏿",
+ "unicodeVersion": "8.0",
"digest": "66656b5e3d0f43f16f983f9db6214b07aac73b143eeff6475782f98aa5b9ba53"
},
- {
- "name": "hospital",
- "unicode": "1F3E5",
+ "hospital": {
+ "category": "travel",
+ "moji": "🏥",
+ "unicodeVersion": "6.0",
"digest": "034573e76df444f5b0eb7aff3a4103e4b49a1813869155ab3ae29a6fc0c6c8a2"
},
- {
- "name": "hot_pepper",
- "unicode": "1F336",
+ "hot_pepper": {
+ "category": "food",
+ "moji": "🌶",
+ "unicodeVersion": "7.0",
"digest": "0b05777d42698196a10db17d04030175b1dfa772d06288f71d666d5f8d3fddbc"
},
- {
- "name": "hotdog",
- "unicode": "1F32D",
+ "hotdog": {
+ "category": "food",
+ "moji": "🌭",
+ "unicodeVersion": "8.0",
"digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
},
- {
- "name": "hot_dog",
- "unicode": "1F32D",
- "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5"
- },
- {
- "name": "hotel",
- "unicode": "1F3E8",
+ "hotel": {
+ "category": "travel",
+ "moji": "🏨",
+ "unicodeVersion": "6.0",
"digest": "2d78e0ad4cfb0caad778c7de49fefd6e8356afe902a43e3f1c40bceb6b0be422"
},
- {
- "name": "hotsprings",
- "unicode": "2668",
+ "hotsprings": {
+ "category": "symbols",
+ "moji": "♨",
+ "unicodeVersion": "1.1",
"digest": "4c10c3a974b44693e8cbe91365c8b8d7f14f62db234cc516b6e54c08a6bacaed"
},
- {
- "name": "hourglass",
- "unicode": "231B",
+ "hourglass": {
+ "category": "objects",
+ "moji": "⌛",
+ "unicodeVersion": "1.1",
"digest": "f0bae8392aaf6f75a83f5d8914936b8650665b24ba1b232fa546b71545dd9acd"
},
- {
- "name": "hourglass_flowing_sand",
- "unicode": "23F3",
+ "hourglass_flowing_sand": {
+ "category": "objects",
+ "moji": "⏳",
+ "unicodeVersion": "6.0",
"digest": "2d077729f40fc04007a933e97356bd511cbd8be76b8c55962ca3fa0d8b828e23"
},
- {
- "name": "house",
- "unicode": "1F3E0",
+ "house": {
+ "category": "travel",
+ "moji": "🏠",
+ "unicodeVersion": "6.0",
"digest": "b4ac25979fbe161ada0d2a75769aa7552d2371d37d78cddba4ffdc7f076d3279"
},
- {
- "name": "house_abandoned",
- "unicode": "1F3DA",
- "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
- },
- {
- "name": "derelict_house_building",
- "unicode": "1F3DA",
+ "house_abandoned": {
+ "category": "travel",
+ "moji": "🏚",
+ "unicodeVersion": "7.0",
"digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610"
},
- {
- "name": "house_with_garden",
- "unicode": "1F3E1",
+ "house_with_garden": {
+ "category": "travel",
+ "moji": "🏡",
+ "unicodeVersion": "6.0",
"digest": "817463f23ec0a849393ba75c333e822b4d253cd4db998c127e90d1b924f35d20"
},
- {
- "name": "hugging",
- "unicode": "1F917",
+ "hugging": {
+ "category": "people",
+ "moji": "🤗",
+ "unicodeVersion": "8.0",
"digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
},
- {
- "name": "hugging_face",
- "unicode": "1F917",
- "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f"
- },
- {
- "name": "hushed",
- "unicode": "1F62F",
+ "hushed": {
+ "category": "people",
+ "moji": "😯",
+ "unicodeVersion": "6.1",
"digest": "22586107f7399eff64538a52929dade152633aa268fc5ec4e6fe1c0e00a7bd89"
},
- {
- "name": "ice_cream",
- "unicode": "1F368",
+ "ice_cream": {
+ "category": "food",
+ "moji": "🍨",
+ "unicodeVersion": "6.0",
"digest": "d1a8e685f2ecf83dead28733859e369d6ce120a2669cdab97dc4423547d472ac"
},
- {
- "name": "ice_skate",
- "unicode": "26F8",
+ "ice_skate": {
+ "category": "activity",
+ "moji": "⛸",
+ "unicodeVersion": "5.2",
"digest": "41ef65c143bc068868fa64080ffd447d91aa3fe2a39e69ecaa97022820af4dcd"
},
- {
- "name": "icecream",
- "unicode": "1F366",
+ "icecream": {
+ "category": "food",
+ "moji": "🍦",
+ "unicodeVersion": "6.0",
"digest": "22cfe17b80cbd2a0377ee90da45bd40d33533c914b2639d363fbb1f00714e194"
},
- {
- "name": "id",
- "unicode": "1F194",
+ "id": {
+ "category": "symbols",
+ "moji": "🆔",
+ "unicodeVersion": "6.0",
"digest": "bcf0922e083821d3be7951893084ea0d72a0110ef0b20d11dfec24dd70633893"
},
- {
- "name": "ideograph_advantage",
- "unicode": "1F250",
+ "ideograph_advantage": {
+ "category": "symbols",
+ "moji": "🉐",
+ "unicodeVersion": "6.0",
"digest": "0b6bf59f63fda1afa92d652814a778a056c3f4abdd9cf3f6796068bd71783051"
},
- {
- "name": "imp",
- "unicode": "1F47F",
+ "imp": {
+ "category": "people",
+ "moji": "👿",
+ "unicodeVersion": "6.0",
"digest": "52598cf2441988f875ccb4e479637baefc679e3ca64e9a6400e56488b0fde811"
},
- {
- "name": "inbox_tray",
- "unicode": "1F4E5",
+ "inbox_tray": {
+ "category": "objects",
+ "moji": "📥",
+ "unicodeVersion": "6.0",
"digest": "d5d9497022b5318fcfbfdfcd56df9c65dd8f4a4cb5e6283ca260836df57da301"
},
- {
- "name": "incoming_envelope",
- "unicode": "1F4E8",
+ "incoming_envelope": {
+ "category": "objects",
+ "moji": "📨",
+ "unicodeVersion": "6.0",
"digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a"
},
- {
- "name": "information_desk_person",
- "unicode": "1F481",
+ "information_desk_person": {
+ "category": "people",
+ "moji": "💁",
+ "unicodeVersion": "6.0",
"digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064"
},
- {
- "name": "information_desk_person_tone1",
- "unicode": "1F481-1F3FB",
+ "information_desk_person_tone1": {
+ "category": "people",
+ "moji": "💁🏻",
+ "unicodeVersion": "8.0",
"digest": "6674f2e059eff7cfd7fd6abc800da37c4f1087feb4ff26c9e4e31aa29fdf9921"
},
- {
- "name": "information_desk_person_tone2",
- "unicode": "1F481-1F3FC",
+ "information_desk_person_tone2": {
+ "category": "people",
+ "moji": "💁🏼",
+ "unicodeVersion": "8.0",
"digest": "9983412ecd130b7e9cfb078167016c06fd043b6f9f3c26d21733ca3f059fd109"
},
- {
- "name": "information_desk_person_tone3",
- "unicode": "1F481-1F3FD",
+ "information_desk_person_tone3": {
+ "category": "people",
+ "moji": "💁🏽",
+ "unicodeVersion": "8.0",
"digest": "d8907bf47af5722127afca8fc0da587eab33044a6c60a94890983deb8d6f7a66"
},
- {
- "name": "information_desk_person_tone4",
- "unicode": "1F481-1F3FE",
+ "information_desk_person_tone4": {
+ "category": "people",
+ "moji": "💁🏾",
+ "unicodeVersion": "8.0",
"digest": "3be086d4edfe9ca8e4a364b4e8d09b81b5b594b5eeb9ffdf6370179fb3118658"
},
- {
- "name": "information_desk_person_tone5",
- "unicode": "1F481-1F3FF",
+ "information_desk_person_tone5": {
+ "category": "people",
+ "moji": "💁🏿",
+ "unicodeVersion": "8.0",
"digest": "2fde4e98dd11c5c29c89cad7cbb7bd2d5077dfad07913b20e01955b2d0dfad40"
},
- {
- "name": "information_source",
- "unicode": "2139",
+ "information_source": {
+ "category": "symbols",
+ "moji": "ℹ",
+ "unicodeVersion": "3.0",
"digest": "b6bf3cce86d42c2e3c46470baab4af01e900b8ae337b605c3da07c3eba671269"
},
- {
- "name": "innocent",
- "unicode": "1F607",
+ "innocent": {
+ "category": "people",
+ "moji": "😇",
+ "unicodeVersion": "6.0",
"digest": "20f8d856bc3e46f4b1173cea05d4577e1c61f06b2daba46e57db90f4066bb428"
},
- {
- "name": "interrobang",
- "unicode": "2049",
+ "interrobang": {
+ "category": "symbols",
+ "moji": "⁉",
+ "unicodeVersion": "3.0",
"digest": "92a2d5b4c0bd6714e402f6f12fe19774cb41d081b5e9c23c415ce794224d8117"
},
- {
- "name": "iphone",
- "unicode": "1F4F1",
+ "iphone": {
+ "category": "objects",
+ "moji": "📱",
+ "unicodeVersion": "6.0",
"digest": "1ebc54215713cd4bf1c1e50770999f2512bb4fea29e37d0bb3a8aa2460ff875d"
},
- {
- "name": "island",
- "unicode": "1F3DD",
- "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
- },
- {
- "name": "desert_island",
- "unicode": "1F3DD",
+ "island": {
+ "category": "travel",
+ "moji": "🏝",
+ "unicodeVersion": "7.0",
"digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d"
},
- {
- "name": "izakaya_lantern",
- "unicode": "1F3EE",
+ "izakaya_lantern": {
+ "category": "objects",
+ "moji": "🏮",
+ "unicodeVersion": "6.0",
"digest": "fbdc290e666d43d0776a73b955c26df4518692b35e72742e073705fc4ca2ae88"
},
- {
- "name": "jack_o_lantern",
- "unicode": "1F383",
+ "jack_o_lantern": {
+ "category": "nature",
+ "moji": "🎃",
+ "unicodeVersion": "6.0",
"digest": "78d666c2e80f64bfb6796f53e5ba4960a83ec36192110e8661031bee2b5e370a"
},
- {
- "name": "japan",
- "unicode": "1F5FE",
+ "japan": {
+ "category": "travel",
+ "moji": "🗾",
+ "unicodeVersion": "6.0",
"digest": "e7d9d6ebf9047fdd3c52e074ba259659c6d8e51a6abae3cdb8d6cf6dbf9a93fe"
},
- {
- "name": "japanese_castle",
- "unicode": "1F3EF",
+ "japanese_castle": {
+ "category": "travel",
+ "moji": "🏯",
+ "unicodeVersion": "6.0",
"digest": "938ae132c403330288223b88d28c19a47224d4f254fbc2366ecef73d9633112c"
},
- {
- "name": "japanese_goblin",
- "unicode": "1F47A",
+ "japanese_goblin": {
+ "category": "people",
+ "moji": "👺",
+ "unicodeVersion": "6.0",
"digest": "63d4bcf58b9d0c29612994432aad2ae35819fdd2890674e60a2f1d51601b742e"
},
- {
- "name": "japanese_ogre",
- "unicode": "1F479",
+ "japanese_ogre": {
+ "category": "people",
+ "moji": "👹",
+ "unicodeVersion": "6.0",
"digest": "434ceedd102e7dcbc07e086811673dd63659ddf8c3ec4d029a3d759a0abfcbdb"
},
- {
- "name": "jeans",
- "unicode": "1F456",
+ "jeans": {
+ "category": "people",
+ "moji": "👖",
+ "unicodeVersion": "6.0",
"digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5"
},
- {
- "name": "joy",
- "unicode": "1F602",
+ "joy": {
+ "category": "people",
+ "moji": "😂",
+ "unicodeVersion": "6.0",
"digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08"
},
- {
- "name": "joy_cat",
- "unicode": "1F639",
+ "joy_cat": {
+ "category": "people",
+ "moji": "😹",
+ "unicodeVersion": "6.0",
"digest": "a65c999604147e5e20170fcb14f80a1ff0a633f991492e1f790b2ad4caec7b7e"
},
- {
- "name": "joystick",
- "unicode": "1F579",
+ "joystick": {
+ "category": "objects",
+ "moji": "🕹",
+ "unicodeVersion": "7.0",
"digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd"
},
- {
- "name": "juggling",
- "unicode": "1F939",
+ "juggling": {
+ "category": "activity",
+ "moji": "🤹",
+ "unicodeVersion": "9.0",
"digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
},
- {
- "name": "juggler",
- "unicode": "1F939",
- "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
- },
- {
- "name": "juggling_tone1",
- "unicode": "1F939-1F3FB",
- "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
- },
- {
- "name": "juggler_tone1",
- "unicode": "1F939-1F3FB",
+ "juggling_tone1": {
+ "category": "activity",
+ "moji": "🤹🏻",
+ "unicodeVersion": "9.0",
"digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
},
- {
- "name": "juggling_tone2",
- "unicode": "1F939-1F3FC",
+ "juggling_tone2": {
+ "category": "activity",
+ "moji": "🤹🏼",
+ "unicodeVersion": "9.0",
"digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
},
- {
- "name": "juggler_tone2",
- "unicode": "1F939-1F3FC",
- "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
- },
- {
- "name": "juggling_tone3",
- "unicode": "1F939-1F3FD",
- "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
- },
- {
- "name": "juggler_tone3",
- "unicode": "1F939-1F3FD",
+ "juggling_tone3": {
+ "category": "activity",
+ "moji": "🤹🏽",
+ "unicodeVersion": "9.0",
"digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
},
- {
- "name": "juggling_tone4",
- "unicode": "1F939-1F3FE",
+ "juggling_tone4": {
+ "category": "activity",
+ "moji": "🤹🏾",
+ "unicodeVersion": "9.0",
"digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
},
- {
- "name": "juggler_tone4",
- "unicode": "1F939-1F3FE",
- "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
- },
- {
- "name": "juggling_tone5",
- "unicode": "1F939-1F3FF",
- "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
- },
- {
- "name": "juggler_tone5",
- "unicode": "1F939-1F3FF",
+ "juggling_tone5": {
+ "category": "activity",
+ "moji": "🤹🏿",
+ "unicodeVersion": "9.0",
"digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
},
- {
- "name": "kaaba",
- "unicode": "1F54B",
+ "kaaba": {
+ "category": "travel",
+ "moji": "🕋",
+ "unicodeVersion": "8.0",
"digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6"
},
- {
- "name": "key",
- "unicode": "1F511",
+ "key": {
+ "category": "objects",
+ "moji": "🔑",
+ "unicodeVersion": "6.0",
"digest": "66719fa77a50a0827c8d47237e2704c03e38186e6fef80627a765473b2294c2e"
},
- {
- "name": "key2",
- "unicode": "1F5DD",
+ "key2": {
+ "category": "objects",
+ "moji": "🗝",
+ "unicodeVersion": "7.0",
"digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
},
- {
- "name": "old_key",
- "unicode": "1F5DD",
- "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e"
- },
- {
- "name": "keyboard",
- "unicode": "2328",
+ "keyboard": {
+ "category": "objects",
+ "moji": "⌨",
+ "unicodeVersion": "1.1",
"digest": "34da8ff62ca964142f9281b80123dbba74deaac8d77fa61758c30cfb36c31386"
},
- {
- "name": "kimono",
- "unicode": "1F458",
+ "kimono": {
+ "category": "people",
+ "moji": "👘",
+ "unicodeVersion": "6.0",
"digest": "637182590e256c8fb74ce4c0565f5180c07f06e3bdebf30138ed3259b209c27f"
},
- {
- "name": "kiss",
- "unicode": "1F48B",
+ "kiss": {
+ "category": "people",
+ "moji": "💋",
+ "unicodeVersion": "6.0",
"digest": "62f9b9ffcb01558cd5bb829344a1d1d399511663ff5235405c1f786c9416a94d"
},
- {
- "name": "kiss_mm",
- "unicode": "1F468-2764-1F48B-1F468",
- "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
- },
- {
- "name": "couplekiss_mm",
- "unicode": "1F468-2764-1F48B-1F468",
+ "kiss_mm": {
+ "category": "people",
+ "moji": "👨‍❤️‍💋‍👨",
+ "unicodeVersion": "6.0",
"digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4"
},
- {
- "name": "kiss_ww",
- "unicode": "1F469-2764-1F48B-1F469",
+ "kiss_ww": {
+ "category": "people",
+ "moji": "👩‍❤️‍💋‍👩",
+ "unicodeVersion": "6.0",
"digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
},
- {
- "name": "couplekiss_ww",
- "unicode": "1F469-2764-1F48B-1F469",
- "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a"
- },
- {
- "name": "kissing",
- "unicode": "1F617",
+ "kissing": {
+ "category": "people",
+ "moji": "😗",
+ "unicodeVersion": "6.1",
"digest": "b4a505f9e3d7fbd0ac60111f0e678cf425a5fd1abc65a3e9db59ae4abcfb8e85"
},
- {
- "name": "kissing_cat",
- "unicode": "1F63D",
+ "kissing_cat": {
+ "category": "people",
+ "moji": "😽",
+ "unicodeVersion": "6.0",
"digest": "a00431bf10601db4998e78433279167e52cbd36aed885399482529d5cdab8636"
},
- {
- "name": "kissing_closed_eyes",
- "unicode": "1F61A",
+ "kissing_closed_eyes": {
+ "category": "people",
+ "moji": "😚",
+ "unicodeVersion": "6.0",
"digest": "ae474db7daf80fe0b82ae1f2a11672cfcd9f9126e100f6e6d4b8a0d135dce39d"
},
- {
- "name": "kissing_heart",
- "unicode": "1F618",
+ "kissing_heart": {
+ "category": "people",
+ "moji": "😘",
+ "unicodeVersion": "6.0",
"digest": "bce372573bd3b347b555c1cd22087e03e650df73c8e0284ab668bf6633251632"
},
- {
- "name": "kissing_smiling_eyes",
- "unicode": "1F619",
+ "kissing_smiling_eyes": {
+ "category": "people",
+ "moji": "😙",
+ "unicodeVersion": "6.1",
"digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f"
},
- {
- "name": "kiwi",
- "unicode": "1F95D",
- "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
- },
- {
- "name": "kiwifruit",
- "unicode": "1F95D",
+ "kiwi": {
+ "category": "food",
+ "moji": "🥝",
+ "unicodeVersion": "9.0",
"digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
},
- {
- "name": "knife",
- "unicode": "1F52A",
+ "knife": {
+ "category": "objects",
+ "moji": "🔪",
+ "unicodeVersion": "6.0",
"digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df"
},
- {
- "name": "koala",
- "unicode": "1F428",
+ "koala": {
+ "category": "nature",
+ "moji": "🐨",
+ "unicodeVersion": "6.0",
"digest": "c58f7e0abae42c2218a85efed0e04151df67187815bebca7f3db6f435e0dab4d"
},
- {
- "name": "koko",
- "unicode": "1F201",
+ "koko": {
+ "category": "symbols",
+ "moji": "🈁",
+ "unicodeVersion": "6.0",
"digest": "5f45eb49bbf298e1fadedfe6cccc297850fcaaa4535e4cc911d48d979af55807"
},
- {
- "name": "label",
- "unicode": "1F3F7",
+ "label": {
+ "category": "objects",
+ "moji": "🏷",
+ "unicodeVersion": "7.0",
"digest": "9550ed50cedbc56eb1bd22a8a0809d837048a33d6e2e6e7d65c50d95fa05a85d"
},
- {
- "name": "large_blue_circle",
- "unicode": "1F535",
+ "large_blue_circle": {
+ "category": "symbols",
+ "moji": "🔵",
+ "unicodeVersion": "6.0",
"digest": "0df3fb3b09a6269459a3d9a1fe78db572190a948680844cfe758f53b6a482ff4"
},
- {
- "name": "large_blue_diamond",
- "unicode": "1F537",
+ "large_blue_diamond": {
+ "category": "symbols",
+ "moji": "🔷",
+ "unicodeVersion": "6.0",
"digest": "7f646b4e9de2788ed09e45f72cb512c269dda4989029b39bf9a2556659321651"
},
- {
- "name": "large_orange_diamond",
- "unicode": "1F536",
+ "large_orange_diamond": {
+ "category": "symbols",
+ "moji": "🔶",
+ "unicodeVersion": "6.0",
"digest": "80ae005ef9d79190c777f00de0993f8b3cb783f7051d76e971640c8c0827c338"
},
- {
- "name": "last_quarter_moon",
- "unicode": "1F317",
+ "last_quarter_moon": {
+ "category": "nature",
+ "moji": "🌗",
+ "unicodeVersion": "6.0",
"digest": "3d1f276607c685d50f4b70d00a57750a57ad9ad84256dafd2dc8eef8c72300c3"
},
- {
- "name": "last_quarter_moon_with_face",
- "unicode": "1F31C",
+ "last_quarter_moon_with_face": {
+ "category": "nature",
+ "moji": "🌜",
+ "unicodeVersion": "6.0",
"digest": "d516825ba52dc67f5a01433fb9df2aa77742d38efde4225983ebc4882cbdfe5d"
},
- {
- "name": "laughing",
- "unicode": "1F606",
+ "laughing": {
+ "category": "people",
+ "moji": "😆",
+ "unicodeVersion": "6.0",
"digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
},
- {
- "name": "satisfied",
- "unicode": "1F606",
- "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81"
- },
- {
- "name": "leaves",
- "unicode": "1F343",
+ "leaves": {
+ "category": "nature",
+ "moji": "🍃",
+ "unicodeVersion": "6.0",
"digest": "56a7a0e767a6f214d340d1b5989efd99fec52c6aa306ec5c3328e32234a1631b"
},
- {
- "name": "ledger",
- "unicode": "1F4D2",
+ "ledger": {
+ "category": "objects",
+ "moji": "📒",
+ "unicodeVersion": "6.0",
"digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4"
},
- {
- "name": "left_facing_fist",
- "unicode": "1F91B",
+ "left_facing_fist": {
+ "category": "people",
+ "moji": "🤛",
+ "unicodeVersion": "9.0",
"digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
},
- {
- "name": "left_fist",
- "unicode": "1F91B",
- "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
- },
- {
- "name": "left_facing_fist_tone1",
- "unicode": "1F91B-1F3FB",
+ "left_facing_fist_tone1": {
+ "category": "people",
+ "moji": "🤛🏻",
+ "unicodeVersion": "9.0",
"digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
},
- {
- "name": "left_fist_tone1",
- "unicode": "1F91B-1F3FB",
- "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
- },
- {
- "name": "left_facing_fist_tone2",
- "unicode": "1F91B-1F3FC",
- "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
- },
- {
- "name": "left_fist_tone2",
- "unicode": "1F91B-1F3FC",
+ "left_facing_fist_tone2": {
+ "category": "people",
+ "moji": "🤛🏼",
+ "unicodeVersion": "9.0",
"digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
},
- {
- "name": "left_facing_fist_tone3",
- "unicode": "1F91B-1F3FD",
+ "left_facing_fist_tone3": {
+ "category": "people",
+ "moji": "🤛🏽",
+ "unicodeVersion": "9.0",
"digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
},
- {
- "name": "left_fist_tone3",
- "unicode": "1F91B-1F3FD",
- "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
- },
- {
- "name": "left_facing_fist_tone4",
- "unicode": "1F91B-1F3FE",
- "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
- },
- {
- "name": "left_fist_tone4",
- "unicode": "1F91B-1F3FE",
+ "left_facing_fist_tone4": {
+ "category": "people",
+ "moji": "🤛🏾",
+ "unicodeVersion": "9.0",
"digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
},
- {
- "name": "left_facing_fist_tone5",
- "unicode": "1F91B-1F3FF",
- "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
- },
- {
- "name": "left_fist_tone5",
- "unicode": "1F91B-1F3FF",
+ "left_facing_fist_tone5": {
+ "category": "people",
+ "moji": "🤛🏿",
+ "unicodeVersion": "9.0",
"digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
},
- {
- "name": "left_luggage",
- "unicode": "1F6C5",
+ "left_luggage": {
+ "category": "symbols",
+ "moji": "🛅",
+ "unicodeVersion": "6.0",
"digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf"
},
- {
- "name": "left_right_arrow",
- "unicode": "2194",
+ "left_right_arrow": {
+ "category": "symbols",
+ "moji": "↔",
+ "unicodeVersion": "1.1",
"digest": "560fcf1b794eb0d5269c73b3f8da57540cbb8a6f1a9af7a9d10b202252247e34"
},
- {
- "name": "leftwards_arrow_with_hook",
- "unicode": "21A9",
+ "leftwards_arrow_with_hook": {
+ "category": "symbols",
+ "moji": "↩",
+ "unicodeVersion": "1.1",
"digest": "504714c5559b1bd35aa469be83069a923d1a25f364cac08c10df0195749e7b26"
},
- {
- "name": "lemon",
- "unicode": "1F34B",
+ "lemon": {
+ "category": "food",
+ "moji": "🍋",
+ "unicodeVersion": "6.0",
"digest": "ccca25bb6ac47770dba3aaf75144128f9a73299061969b25a35ad1733dcde5fe"
},
- {
- "name": "leo",
- "unicode": "264C",
+ "leo": {
+ "category": "symbols",
+ "moji": "♌",
+ "unicodeVersion": "1.1",
"digest": "f2ed930e279699962f189e0cac519cc29d339b3e82debfdc90c5b0935a7543bb"
},
- {
- "name": "leopard",
- "unicode": "1F406",
+ "leopard": {
+ "category": "nature",
+ "moji": "🐆",
+ "unicodeVersion": "6.0",
"digest": "d4a8964b6f2cdf6ddf074d0f1f2f65783a1a43eb4af426905fad0e60899939c7"
},
- {
- "name": "level_slider",
- "unicode": "1F39A",
+ "level_slider": {
+ "category": "objects",
+ "moji": "🎚",
+ "unicodeVersion": "7.0",
"digest": "48842324f54d971ebf548a89a82ac7f29e235702081c91b477b1a92d427290e7"
},
- {
- "name": "levitate",
- "unicode": "1F574",
- "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
- },
- {
- "name": "man_in_business_suit_levitating",
- "unicode": "1F574",
+ "levitate": {
+ "category": "activity",
+ "moji": "🕴",
+ "unicodeVersion": "7.0",
"digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b"
},
- {
- "name": "libra",
- "unicode": "264E",
+ "libra": {
+ "category": "symbols",
+ "moji": "♎",
+ "unicodeVersion": "1.1",
"digest": "e330ba05bb449db074bc23d1514246ca5e249110f44ddb5804e5510eef6deac1"
},
- {
- "name": "lifter",
- "unicode": "1F3CB",
+ "lifter": {
+ "category": "activity",
+ "moji": "🏋",
+ "unicodeVersion": "7.0",
"digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
},
- {
- "name": "weight_lifter",
- "unicode": "1F3CB",
- "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558"
- },
- {
- "name": "lifter_tone1",
- "unicode": "1F3CB-1F3FB",
- "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
- },
- {
- "name": "weight_lifter_tone1",
- "unicode": "1F3CB-1F3FB",
+ "lifter_tone1": {
+ "category": "activity",
+ "moji": "🏋🏻",
+ "unicodeVersion": "8.0",
"digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9"
},
- {
- "name": "lifter_tone2",
- "unicode": "1F3CB-1F3FC",
- "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
- },
- {
- "name": "weight_lifter_tone2",
- "unicode": "1F3CB-1F3FC",
+ "lifter_tone2": {
+ "category": "activity",
+ "moji": "🏋🏼",
+ "unicodeVersion": "8.0",
"digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576"
},
- {
- "name": "lifter_tone3",
- "unicode": "1F3CB-1F3FD",
- "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
- },
- {
- "name": "weight_lifter_tone3",
- "unicode": "1F3CB-1F3FD",
+ "lifter_tone3": {
+ "category": "activity",
+ "moji": "🏋🏽",
+ "unicodeVersion": "8.0",
"digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c"
},
- {
- "name": "lifter_tone4",
- "unicode": "1F3CB-1F3FE",
- "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
- },
- {
- "name": "weight_lifter_tone4",
- "unicode": "1F3CB-1F3FE",
+ "lifter_tone4": {
+ "category": "activity",
+ "moji": "🏋🏾",
+ "unicodeVersion": "8.0",
"digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c"
},
- {
- "name": "lifter_tone5",
- "unicode": "1F3CB-1F3FF",
- "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
- },
- {
- "name": "weight_lifter_tone5",
- "unicode": "1F3CB-1F3FF",
+ "lifter_tone5": {
+ "category": "activity",
+ "moji": "🏋🏿",
+ "unicodeVersion": "8.0",
"digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
},
- {
- "name": "light_rail",
- "unicode": "1F688",
+ "light_rail": {
+ "category": "travel",
+ "moji": "🚈",
+ "unicodeVersion": "6.0",
"digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1"
},
- {
- "name": "link",
- "unicode": "1F517",
+ "link": {
+ "category": "objects",
+ "moji": "🔗",
+ "unicodeVersion": "6.0",
"digest": "7bf567aabd1fc38b3d70422f9db3a13b50950cf6207e70962c9938827c196ccb"
},
- {
- "name": "lion_face",
- "unicode": "1F981",
- "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
- },
- {
- "name": "lion",
- "unicode": "1F981",
+ "lion_face": {
+ "category": "nature",
+ "moji": "🦁",
+ "unicodeVersion": "8.0",
"digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa"
},
- {
- "name": "lips",
- "unicode": "1F444",
+ "lips": {
+ "category": "people",
+ "moji": "👄",
+ "unicodeVersion": "6.0",
"digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e"
},
- {
- "name": "lipstick",
- "unicode": "1F484",
+ "lipstick": {
+ "category": "people",
+ "moji": "💄",
+ "unicodeVersion": "6.0",
"digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1"
},
- {
- "name": "lizard",
- "unicode": "1F98E",
+ "lizard": {
+ "category": "nature",
+ "moji": "🦎",
+ "unicodeVersion": "9.0",
"digest": "fb9191f9eab58b8403d4c4626ccbb14ba05c1f6944011751a8edcc4dd03c66e6"
},
- {
- "name": "lock",
- "unicode": "1F512",
+ "lock": {
+ "category": "objects",
+ "moji": "🔒",
+ "unicodeVersion": "6.0",
"digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83"
},
- {
- "name": "lock_with_ink_pen",
- "unicode": "1F50F",
+ "lock_with_ink_pen": {
+ "category": "objects",
+ "moji": "🔏",
+ "unicodeVersion": "6.0",
"digest": "7b5e959b26cf7296c7b230fc2be9feb9e38391c5001951a019d16b169a71aba9"
},
- {
- "name": "lollipop",
- "unicode": "1F36D",
+ "lollipop": {
+ "category": "food",
+ "moji": "🍭",
+ "unicodeVersion": "6.0",
"digest": "17b6a0df47ec758a2f9c087b46a6902cee344d39407ef4c321e408505cbb72ca"
},
- {
- "name": "loop",
- "unicode": "27BF",
+ "loop": {
+ "category": "symbols",
+ "moji": "➿",
+ "unicodeVersion": "6.0",
"digest": "9f20ecc34b3c871789ba7d0712aa31e7a74b6c1558ac8bea385bc40590056726"
},
- {
- "name": "loud_sound",
- "unicode": "1F50A",
+ "loud_sound": {
+ "category": "symbols",
+ "moji": "🔊",
+ "unicodeVersion": "6.0",
"digest": "64b12db9ddd8adf74a9fc2bd83c7979ea865113347f7ce8666e9ccf5019e715f"
},
- {
- "name": "loudspeaker",
- "unicode": "1F4E2",
+ "loudspeaker": {
+ "category": "symbols",
+ "moji": "📢",
+ "unicodeVersion": "6.0",
"digest": "1e1f35d16dd2898ebaa6f2b2868203df6e09c8a70df069c92d6d1b5cb2ac0976"
},
- {
- "name": "love_hotel",
- "unicode": "1F3E9",
+ "love_hotel": {
+ "category": "travel",
+ "moji": "🏩",
+ "unicodeVersion": "6.0",
"digest": "ff8966a50fd47a216855488eb09a367d231fea21f49e7e5325191d32fb494473"
},
- {
- "name": "love_letter",
- "unicode": "1F48C",
+ "love_letter": {
+ "category": "objects",
+ "moji": "💌",
+ "unicodeVersion": "6.0",
"digest": "037261c8ca4d72f7205e51664591696da2ae7ceb19f1c1c9f6123da5a5979d29"
},
- {
- "name": "low_brightness",
- "unicode": "1F505",
+ "low_brightness": {
+ "category": "symbols",
+ "moji": "🔅",
+ "unicodeVersion": "6.0",
"digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4"
},
- {
- "name": "lying_face",
- "unicode": "1F925",
- "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
- },
- {
- "name": "liar",
- "unicode": "1F925",
+ "lying_face": {
+ "category": "people",
+ "moji": "🤥",
+ "unicodeVersion": "9.0",
"digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
},
- {
- "name": "m",
- "unicode": "24C2",
+ "m": {
+ "category": "symbols",
+ "moji": "Ⓜ",
+ "unicodeVersion": "1.1",
"digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4"
},
- {
- "name": "mag",
- "unicode": "1F50D",
+ "mag": {
+ "category": "objects",
+ "moji": "🔍",
+ "unicodeVersion": "6.0",
"digest": "a6e31a2efa7d9427aaa30b45d9f4181ee55c44be08aea2df165a86e0e6d9eaa1"
},
- {
- "name": "mag_right",
- "unicode": "1F50E",
+ "mag_right": {
+ "category": "objects",
+ "moji": "🔎",
+ "unicodeVersion": "6.0",
"digest": "c7d8ceeb05db261e5eaab31dc4da432d0d5592a2ed71e526c5a542daa230bbaf"
},
- {
- "name": "mahjong",
- "unicode": "1F004",
+ "mahjong": {
+ "category": "symbols",
+ "moji": "🀄",
+ "unicodeVersion": "5.1",
"digest": "755d69f988434ce1c17531a8b7ac92ead6f5607c2635a22f10e0ad70f09fc3e6"
},
- {
- "name": "mailbox",
- "unicode": "1F4EB",
+ "mailbox": {
+ "category": "objects",
+ "moji": "📫",
+ "unicodeVersion": "6.0",
"digest": "2069091be90a530a43ef29d5ec7688c351bf4d5b08d63a0d20d72b67d639ec62"
},
- {
- "name": "mailbox_closed",
- "unicode": "1F4EA",
+ "mailbox_closed": {
+ "category": "objects",
+ "moji": "📪",
+ "unicodeVersion": "6.0",
"digest": "d88d65bfebb8216535fd055c69f319564b2cf0b0901820f8312f581864557ed4"
},
- {
- "name": "mailbox_with_mail",
- "unicode": "1F4EC",
+ "mailbox_with_mail": {
+ "category": "objects",
+ "moji": "📬",
+ "unicodeVersion": "6.0",
"digest": "69e966b4659128991a70c6a2dd4d647551bedb91bdf5ce688958686bbec56381"
},
- {
- "name": "mailbox_with_no_mail",
- "unicode": "1F4ED",
+ "mailbox_with_no_mail": {
+ "category": "objects",
+ "moji": "📭",
+ "unicodeVersion": "6.0",
"digest": "9e92d8ee88f660ce56da61077c80ec26c5d8f54ebd2306c4cfa16f6c1b981f83"
},
- {
- "name": "man",
- "unicode": "1F468",
+ "man": {
+ "category": "people",
+ "moji": "👨",
+ "unicodeVersion": "6.0",
"digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7"
},
- {
- "name": "man_dancing",
- "unicode": "1F57A",
- "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
- },
- {
- "name": "male_dancer",
- "unicode": "1F57A",
+ "man_dancing": {
+ "category": "people",
+ "moji": "🕺",
+ "unicodeVersion": "9.0",
"digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
},
- {
- "name": "man_dancing_tone1",
- "unicode": "1F57A-1F3FB",
- "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
- },
- {
- "name": "male_dancer_tone1",
- "unicode": "1F57A-1F3FB",
+ "man_dancing_tone1": {
+ "category": "activity",
+ "moji": "🕺🏻",
+ "unicodeVersion": "9.0",
"digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
},
- {
- "name": "man_dancing_tone2",
- "unicode": "1F57A-1F3FC",
- "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
- },
- {
- "name": "male_dancer_tone2",
- "unicode": "1F57A-1F3FC",
+ "man_dancing_tone2": {
+ "category": "activity",
+ "moji": "🕺🏼",
+ "unicodeVersion": "9.0",
"digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
},
- {
- "name": "man_dancing_tone3",
- "unicode": "1F57A-1F3FD",
+ "man_dancing_tone3": {
+ "category": "activity",
+ "moji": "🕺🏽",
+ "unicodeVersion": "9.0",
"digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
},
- {
- "name": "male_dancer_tone3",
- "unicode": "1F57A-1F3FD",
- "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
- },
- {
- "name": "man_dancing_tone4",
- "unicode": "1F57A-1F3FE",
- "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
- },
- {
- "name": "male_dancer_tone4",
- "unicode": "1F57A-1F3FE",
+ "man_dancing_tone4": {
+ "category": "activity",
+ "moji": "🕺🏾",
+ "unicodeVersion": "9.0",
"digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
},
- {
- "name": "man_dancing_tone5",
- "unicode": "1F57A-1F3FF",
+ "man_dancing_tone5": {
+ "category": "activity",
+ "moji": "🕺🏿",
+ "unicodeVersion": "9.0",
"digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
},
- {
- "name": "male_dancer_tone5",
- "unicode": "1F57A-1F3FF",
- "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
- },
- {
- "name": "man_in_tuxedo",
- "unicode": "1F935",
+ "man_in_tuxedo": {
+ "category": "people",
+ "moji": "🤵",
+ "unicodeVersion": "9.0",
"digest": "4d451a971dfefedc4830ba78e19b123f250e09ae65baddccdc56c0f8aa3a9b50"
},
- {
- "name": "man_in_tuxedo_tone1",
- "unicode": "1F935-1F3FB",
- "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
- },
- {
- "name": "tuxedo_tone1",
- "unicode": "1F935-1F3FB",
+ "man_in_tuxedo_tone1": {
+ "category": "people",
+ "moji": "🤵🏻",
+ "unicodeVersion": "9.0",
"digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
},
- {
- "name": "man_in_tuxedo_tone2",
- "unicode": "1F935-1F3FC",
+ "man_in_tuxedo_tone2": {
+ "category": "people",
+ "moji": "🤵🏼",
+ "unicodeVersion": "9.0",
"digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
},
- {
- "name": "tuxedo_tone2",
- "unicode": "1F935-1F3FC",
- "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
- },
- {
- "name": "man_in_tuxedo_tone3",
- "unicode": "1F935-1F3FD",
- "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
- },
- {
- "name": "tuxedo_tone3",
- "unicode": "1F935-1F3FD",
+ "man_in_tuxedo_tone3": {
+ "category": "people",
+ "moji": "🤵🏽",
+ "unicodeVersion": "9.0",
"digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
},
- {
- "name": "man_in_tuxedo_tone4",
- "unicode": "1F935-1F3FE",
+ "man_in_tuxedo_tone4": {
+ "category": "people",
+ "moji": "🤵🏾",
+ "unicodeVersion": "9.0",
"digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
},
- {
- "name": "tuxedo_tone4",
- "unicode": "1F935-1F3FE",
- "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
- },
- {
- "name": "man_in_tuxedo_tone5",
- "unicode": "1F935-1F3FF",
- "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
- },
- {
- "name": "tuxedo_tone5",
- "unicode": "1F935-1F3FF",
+ "man_in_tuxedo_tone5": {
+ "category": "people",
+ "moji": "🤵🏿",
+ "unicodeVersion": "9.0",
"digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
},
- {
- "name": "man_tone1",
- "unicode": "1F468-1F3FB",
+ "man_tone1": {
+ "category": "people",
+ "moji": "👨🏻",
+ "unicodeVersion": "8.0",
"digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504"
},
- {
- "name": "man_tone2",
- "unicode": "1F468-1F3FC",
+ "man_tone2": {
+ "category": "people",
+ "moji": "👨🏼",
+ "unicodeVersion": "8.0",
"digest": "7ebc64de40d3ac60fb761be5cf94f53fa10b4f03fb66add46c90f5d98eaf71eb"
},
- {
- "name": "man_tone3",
- "unicode": "1F468-1F3FD",
+ "man_tone3": {
+ "category": "people",
+ "moji": "👨🏽",
+ "unicodeVersion": "8.0",
"digest": "77ceef4d3740ed4751acb83dd45b6b754cf625c522c6757309cd4d61202d7149"
},
- {
- "name": "man_tone4",
- "unicode": "1F468-1F3FE",
+ "man_tone4": {
+ "category": "people",
+ "moji": "👨🏾",
+ "unicodeVersion": "8.0",
"digest": "41e6037c393f61cca61b9a81b27ed14a95d75fe380e3a00153c33a371a836ffd"
},
- {
- "name": "man_tone5",
- "unicode": "1F468-1F3FF",
+ "man_tone5": {
+ "category": "people",
+ "moji": "👨🏿",
+ "unicodeVersion": "8.0",
"digest": "a8cebfd39a5b9c79af7cc37f205e1135376056fee287af967c9f55d415572d99"
},
- {
- "name": "man_with_gua_pi_mao",
- "unicode": "1F472",
+ "man_with_gua_pi_mao": {
+ "category": "people",
+ "moji": "👲",
+ "unicodeVersion": "6.0",
"digest": "3dae285e900c69986a48db0fa89d4f371a49f38608059cdae52be098030c5ac4"
},
- {
- "name": "man_with_gua_pi_mao_tone1",
- "unicode": "1F472-1F3FB",
+ "man_with_gua_pi_mao_tone1": {
+ "category": "people",
+ "moji": "👲🏻",
+ "unicodeVersion": "8.0",
"digest": "35404d8e266920c78edd9e7143fb052b42f65242a5698494c4f4365e9183cc67"
},
- {
- "name": "man_with_gua_pi_mao_tone2",
- "unicode": "1F472-1F3FC",
+ "man_with_gua_pi_mao_tone2": {
+ "category": "people",
+ "moji": "👲🏼",
+ "unicodeVersion": "8.0",
"digest": "82d4f968665a93c7543372c8a1eeb0f25d0ea6842d5e518bd91c226c6c3ab8c2"
},
- {
- "name": "man_with_gua_pi_mao_tone3",
- "unicode": "1F472-1F3FD",
+ "man_with_gua_pi_mao_tone3": {
+ "category": "people",
+ "moji": "👲🏽",
+ "unicodeVersion": "8.0",
"digest": "f44159f0c672b9b833449382896180e799abf574f5b3c6cd9541caa992fa18ce"
},
- {
- "name": "man_with_gua_pi_mao_tone4",
- "unicode": "1F472-1F3FE",
+ "man_with_gua_pi_mao_tone4": {
+ "category": "people",
+ "moji": "👲🏾",
+ "unicodeVersion": "8.0",
"digest": "c79060188f9461ca34eaa225b7682d8c410883609509fb731c992db69bfeeb50"
},
- {
- "name": "man_with_gua_pi_mao_tone5",
- "unicode": "1F472-1F3FF",
+ "man_with_gua_pi_mao_tone5": {
+ "category": "people",
+ "moji": "👲🏿",
+ "unicodeVersion": "8.0",
"digest": "de9e4acdb10f7abddeeabc0b48d91139fc8b544a601c530db811f099991b0d38"
},
- {
- "name": "man_with_turban",
- "unicode": "1F473",
+ "man_with_turban": {
+ "category": "people",
+ "moji": "👳",
+ "unicodeVersion": "6.0",
"digest": "db72c944e93983f38d00e3e936ebb5b243c6069f1f1236d46f6a9f1beb8d6634"
},
- {
- "name": "man_with_turban_tone1",
- "unicode": "1F473-1F3FB",
+ "man_with_turban_tone1": {
+ "category": "people",
+ "moji": "👳🏻",
+ "unicodeVersion": "8.0",
"digest": "b6d7489c4cd151af09fff48b62c54c336303e14866e6ef38f94cd834b085d09e"
},
- {
- "name": "man_with_turban_tone2",
- "unicode": "1F473-1F3FC",
+ "man_with_turban_tone2": {
+ "category": "people",
+ "moji": "👳🏼",
+ "unicodeVersion": "8.0",
"digest": "7854ef973c21847f452d7e78e5c460ea300e12b539ce92c69dabe8f1bf3a4382"
},
- {
- "name": "man_with_turban_tone3",
- "unicode": "1F473-1F3FD",
+ "man_with_turban_tone3": {
+ "category": "people",
+ "moji": "👳🏽",
+ "unicodeVersion": "8.0",
"digest": "1dbd9bd78f5263cbadee7d0d5754c14cfbc914f7329e25fbd97d9f5b8ce0737e"
},
- {
- "name": "man_with_turban_tone4",
- "unicode": "1F473-1F3FE",
+ "man_with_turban_tone4": {
+ "category": "people",
+ "moji": "👳🏾",
+ "unicodeVersion": "8.0",
"digest": "4f4804da4a7c98ad4f9db3ae3eaf674c8977c638e73414e33ef1f65098e413a3"
},
- {
- "name": "man_with_turban_tone5",
- "unicode": "1F473-1F3FF",
+ "man_with_turban_tone5": {
+ "category": "people",
+ "moji": "👳🏿",
+ "unicodeVersion": "8.0",
"digest": "240282aa346ef9b1d0d475ea93a02597697f0f56f086305879b532b0b933210a"
},
- {
- "name": "mans_shoe",
- "unicode": "1F45E",
+ "mans_shoe": {
+ "category": "people",
+ "moji": "👞",
+ "unicodeVersion": "6.0",
"digest": "f53fe74abd9906cd3e2dd7e7bddbe1feb9f8f7be28b807fabe452f1f60ca1b84"
},
- {
- "name": "map",
- "unicode": "1F5FA",
+ "map": {
+ "category": "objects",
+ "moji": "🗺",
+ "unicodeVersion": "7.0",
"digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
},
- {
- "name": "world_map",
- "unicode": "1F5FA",
- "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de"
- },
- {
- "name": "maple_leaf",
- "unicode": "1F341",
+ "maple_leaf": {
+ "category": "nature",
+ "moji": "🍁",
+ "unicodeVersion": "6.0",
"digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72"
},
- {
- "name": "martial_arts_uniform",
- "unicode": "1F94B",
- "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
- },
- {
- "name": "karate_uniform",
- "unicode": "1F94B",
+ "martial_arts_uniform": {
+ "category": "activity",
+ "moji": "🥋",
+ "unicodeVersion": "9.0",
"digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
},
- {
- "name": "mask",
- "unicode": "1F637",
+ "mask": {
+ "category": "people",
+ "moji": "😷",
+ "unicodeVersion": "6.0",
"digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600"
},
- {
- "name": "massage",
- "unicode": "1F486",
+ "massage": {
+ "category": "people",
+ "moji": "💆",
+ "unicodeVersion": "6.0",
"digest": "6ee48b4d8cec0bf31e11d7803ad9fc1f909457c8c00cb320b5671395af3c170c"
},
- {
- "name": "massage_tone1",
- "unicode": "1F486-1F3FB",
+ "massage_tone1": {
+ "category": "people",
+ "moji": "💆🏻",
+ "unicodeVersion": "8.0",
"digest": "9da162c2f39628156b87db986a6ada59372a9e9a6b3f0488d21c9e65ec3309bb"
},
- {
- "name": "massage_tone2",
- "unicode": "1F486-1F3FC",
+ "massage_tone2": {
+ "category": "people",
+ "moji": "💆🏼",
+ "unicodeVersion": "8.0",
"digest": "ac259188549b5b429b8c4929e1da2314859e8857ee49720551467aedfcc96567"
},
- {
- "name": "massage_tone3",
- "unicode": "1F486-1F3FD",
+ "massage_tone3": {
+ "category": "people",
+ "moji": "💆🏽",
+ "unicodeVersion": "8.0",
"digest": "cfd9c105b6debc10448f172afcb20d4192899f7ae5aa8af54c834153a5466364"
},
- {
- "name": "massage_tone4",
- "unicode": "1F486-1F3FE",
+ "massage_tone4": {
+ "category": "people",
+ "moji": "💆🏾",
+ "unicodeVersion": "8.0",
"digest": "38ab715c621c58454f3cb09153a96380118cf082568554b6edc5f83fb62e9297"
},
- {
- "name": "massage_tone5",
- "unicode": "1F486-1F3FF",
+ "massage_tone5": {
+ "category": "people",
+ "moji": "💆🏿",
+ "unicodeVersion": "8.0",
"digest": "32480457734121b0c83e9be6d693ae379c95535f43f963c0c2f0f20434ee12c6"
},
- {
- "name": "meat_on_bone",
- "unicode": "1F356",
+ "meat_on_bone": {
+ "category": "food",
+ "moji": "🍖",
+ "unicodeVersion": "6.0",
"digest": "d71a8e0b118d5e6ca60690793ce9649afb78e707fcbd7be890a75564c94434fd"
},
- {
- "name": "medal",
- "unicode": "1F3C5",
+ "medal": {
+ "category": "activity",
+ "moji": "🏅",
+ "unicodeVersion": "7.0",
"digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
},
- {
- "name": "sports_medal",
- "unicode": "1F3C5",
- "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391"
- },
- {
- "name": "mega",
- "unicode": "1F4E3",
+ "mega": {
+ "category": "symbols",
+ "moji": "📣",
+ "unicodeVersion": "6.0",
"digest": "4b1def6b5b051c5045514063f0ac006222ad81fbfe56d840e14bb950713e331b"
},
- {
- "name": "melon",
- "unicode": "1F348",
+ "melon": {
+ "category": "food",
+ "moji": "🍈",
+ "unicodeVersion": "6.0",
"digest": "0cdd663e6f2129808856cdf0746e6571b62aac641f224adb553baf3bb63ba3bd"
},
- {
- "name": "menorah",
- "unicode": "1F54E",
+ "menorah": {
+ "category": "symbols",
+ "moji": "🕎",
+ "unicodeVersion": "8.0",
"digest": "49fca8c3bc00ea69653ee2f8d4e21e561856ba39716c13e9d107db3e805a2997"
},
- {
- "name": "mens",
- "unicode": "1F6B9",
+ "mens": {
+ "category": "symbols",
+ "moji": "🚹",
+ "unicodeVersion": "6.0",
"digest": "7d92292586ee12a5d1a557c37da4d14708dc3ce701cf32d3280dcc83d91e5df8"
},
- {
- "name": "metal",
- "unicode": "1F918",
- "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
- },
- {
- "name": "sign_of_the_horns",
- "unicode": "1F918",
+ "metal": {
+ "category": "people",
+ "moji": "🤘",
+ "unicodeVersion": "8.0",
"digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29"
},
- {
- "name": "metal_tone1",
- "unicode": "1F918-1F3FB",
+ "metal_tone1": {
+ "category": "people",
+ "moji": "🤘🏻",
+ "unicodeVersion": "8.0",
"digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
},
- {
- "name": "sign_of_the_horns_tone1",
- "unicode": "1F918-1F3FB",
- "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d"
- },
- {
- "name": "metal_tone2",
- "unicode": "1F918-1F3FC",
- "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
- },
- {
- "name": "sign_of_the_horns_tone2",
- "unicode": "1F918-1F3FC",
+ "metal_tone2": {
+ "category": "people",
+ "moji": "🤘🏼",
+ "unicodeVersion": "8.0",
"digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb"
},
- {
- "name": "metal_tone3",
- "unicode": "1F918-1F3FD",
+ "metal_tone3": {
+ "category": "people",
+ "moji": "🤘🏽",
+ "unicodeVersion": "8.0",
"digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
},
- {
- "name": "sign_of_the_horns_tone3",
- "unicode": "1F918-1F3FD",
- "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e"
- },
- {
- "name": "metal_tone4",
- "unicode": "1F918-1F3FE",
+ "metal_tone4": {
+ "category": "people",
+ "moji": "🤘🏾",
+ "unicodeVersion": "8.0",
"digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
},
- {
- "name": "sign_of_the_horns_tone4",
- "unicode": "1F918-1F3FE",
- "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8"
- },
- {
- "name": "metal_tone5",
- "unicode": "1F918-1F3FF",
+ "metal_tone5": {
+ "category": "people",
+ "moji": "🤘🏿",
+ "unicodeVersion": "8.0",
"digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
},
- {
- "name": "sign_of_the_horns_tone5",
- "unicode": "1F918-1F3FF",
- "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2"
- },
- {
- "name": "metro",
- "unicode": "1F687",
+ "metro": {
+ "category": "travel",
+ "moji": "🚇",
+ "unicodeVersion": "6.0",
"digest": "b380247b61b5e2ca1b9b70fabff65907b2c3a5191a14b169ae094af94659b9b1"
},
- {
- "name": "microphone",
- "unicode": "1F3A4",
+ "microphone": {
+ "category": "activity",
+ "moji": "🎤",
+ "unicodeVersion": "6.0",
"digest": "9ef4fc2e40d5391c4bb2d30f34f59662cff7cbb1b04341c9dac210d0e21b44ae"
},
- {
- "name": "microphone2",
- "unicode": "1F399",
- "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
- },
- {
- "name": "studio_microphone",
- "unicode": "1F399",
+ "microphone2": {
+ "category": "objects",
+ "moji": "🎙",
+ "unicodeVersion": "7.0",
"digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc"
},
- {
- "name": "microscope",
- "unicode": "1F52C",
+ "microscope": {
+ "category": "objects",
+ "moji": "🔬",
+ "unicodeVersion": "6.0",
"digest": "4ca4322c6ba99b8c15acdb8b605f84f87398769e504b262b134c1f3868b2692f"
},
- {
- "name": "middle_finger",
- "unicode": "1F595",
+ "middle_finger": {
+ "category": "people",
+ "moji": "🖕",
+ "unicodeVersion": "7.0",
"digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
},
- {
- "name": "reversed_hand_with_middle_finger_extended",
- "unicode": "1F595",
- "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e"
- },
- {
- "name": "middle_finger_tone1",
- "unicode": "1F595-1F3FB",
- "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
- },
- {
- "name": "reversed_hand_with_middle_finger_extended_tone1",
- "unicode": "1F595-1F3FB",
+ "middle_finger_tone1": {
+ "category": "people",
+ "moji": "🖕🏻",
+ "unicodeVersion": "8.0",
"digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7"
},
- {
- "name": "middle_finger_tone2",
- "unicode": "1F595-1F3FC",
- "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
- },
- {
- "name": "reversed_hand_with_middle_finger_extended_tone2",
- "unicode": "1F595-1F3FC",
+ "middle_finger_tone2": {
+ "category": "people",
+ "moji": "🖕🏼",
+ "unicodeVersion": "8.0",
"digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83"
},
- {
- "name": "middle_finger_tone3",
- "unicode": "1F595-1F3FD",
- "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
- },
- {
- "name": "reversed_hand_with_middle_finger_extended_tone3",
- "unicode": "1F595-1F3FD",
+ "middle_finger_tone3": {
+ "category": "people",
+ "moji": "🖕🏽",
+ "unicodeVersion": "8.0",
"digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1"
},
- {
- "name": "middle_finger_tone4",
- "unicode": "1F595-1F3FE",
- "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
- },
- {
- "name": "reversed_hand_with_middle_finger_extended_tone4",
- "unicode": "1F595-1F3FE",
+ "middle_finger_tone4": {
+ "category": "people",
+ "moji": "🖕🏾",
+ "unicodeVersion": "8.0",
"digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7"
},
- {
- "name": "middle_finger_tone5",
- "unicode": "1F595-1F3FF",
- "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
- },
- {
- "name": "reversed_hand_with_middle_finger_extended_tone5",
- "unicode": "1F595-1F3FF",
+ "middle_finger_tone5": {
+ "category": "people",
+ "moji": "🖕🏿",
+ "unicodeVersion": "8.0",
"digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575"
},
- {
- "name": "military_medal",
- "unicode": "1F396",
+ "military_medal": {
+ "category": "activity",
+ "moji": "🎖",
+ "unicodeVersion": "7.0",
"digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d"
},
- {
- "name": "milk",
- "unicode": "1F95B",
- "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
- },
- {
- "name": "glass_of_milk",
- "unicode": "1F95B",
+ "milk": {
+ "category": "food",
+ "moji": "🥛",
+ "unicodeVersion": "9.0",
"digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
},
- {
- "name": "milky_way",
- "unicode": "1F30C",
+ "milky_way": {
+ "category": "travel",
+ "moji": "🌌",
+ "unicodeVersion": "6.0",
"digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d"
},
- {
- "name": "minibus",
- "unicode": "1F690",
+ "minibus": {
+ "category": "travel",
+ "moji": "🚐",
+ "unicodeVersion": "6.0",
"digest": "08ccb4b1bf397b7c9aed901e2b5dcdd6cb8ca5c5487ef26775bb3120f7b92524"
},
- {
- "name": "minidisc",
- "unicode": "1F4BD",
+ "minidisc": {
+ "category": "objects",
+ "moji": "💽",
+ "unicodeVersion": "6.0",
"digest": "bebf82c0b91ef66321e7ae7a0abf322e59b2f7d8e6fbf9a94243210c00229c59"
},
- {
- "name": "mobile_phone_off",
- "unicode": "1F4F4",
+ "mobile_phone_off": {
+ "category": "symbols",
+ "moji": "📴",
+ "unicodeVersion": "6.0",
"digest": "6f9d8d6a32fc998f5d8144a5ff7e2ad00de37ad464cd97285e7c72efb09a1feb"
},
- {
- "name": "money_mouth",
- "unicode": "1F911",
+ "money_mouth": {
+ "category": "people",
+ "moji": "🤑",
+ "unicodeVersion": "8.0",
"digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
},
- {
- "name": "money_mouth_face",
- "unicode": "1F911",
- "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e"
- },
- {
- "name": "money_with_wings",
- "unicode": "1F4B8",
+ "money_with_wings": {
+ "category": "objects",
+ "moji": "💸",
+ "unicodeVersion": "6.0",
"digest": "15fcf0595021374ba091ca00efdb4167770da4d421eab930964108545f4edab9"
},
- {
- "name": "moneybag",
- "unicode": "1F4B0",
+ "moneybag": {
+ "category": "objects",
+ "moji": "💰",
+ "unicodeVersion": "6.0",
"digest": "02d708e2f603b0df6f6c169b5c49b3452e1c02e7d72e96f228b73d0b0a20bff4"
},
- {
- "name": "monkey",
- "unicode": "1F412",
+ "monkey": {
+ "category": "nature",
+ "moji": "🐒",
+ "unicodeVersion": "6.0",
"digest": "3588a544d6d9e9995b45d60327a1a42002fa1faa4d48224b140facd249af1c67"
},
- {
- "name": "monkey_face",
- "unicode": "1F435",
+ "monkey_face": {
+ "category": "nature",
+ "moji": "🐵",
+ "unicodeVersion": "6.0",
"digest": "9e263ef5ca42bb76d1b1d1e3cbf020bcf05023a6e9f91301d30c9eb406363a2a"
},
- {
- "name": "monorail",
- "unicode": "1F69D",
+ "monorail": {
+ "category": "travel",
+ "moji": "🚝",
+ "unicodeVersion": "6.0",
"digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad"
},
- {
- "name": "mortar_board",
- "unicode": "1F393",
+ "mortar_board": {
+ "category": "people",
+ "moji": "🎓",
+ "unicodeVersion": "6.0",
"digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410"
},
- {
- "name": "mosque",
- "unicode": "1F54C",
+ "mosque": {
+ "category": "travel",
+ "moji": "🕌",
+ "unicodeVersion": "8.0",
"digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196"
},
- {
- "name": "motor_scooter",
- "unicode": "1F6F5",
- "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
- },
- {
- "name": "motorbike",
- "unicode": "1F6F5",
+ "motor_scooter": {
+ "category": "travel",
+ "moji": "🛵",
+ "unicodeVersion": "9.0",
"digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
},
- {
- "name": "motorboat",
- "unicode": "1F6E5",
+ "motorboat": {
+ "category": "travel",
+ "moji": "🛥",
+ "unicodeVersion": "7.0",
"digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01"
},
- {
- "name": "motorcycle",
- "unicode": "1F3CD",
+ "motorcycle": {
+ "category": "travel",
+ "moji": "🏍",
+ "unicodeVersion": "7.0",
"digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
},
- {
- "name": "racing_motorcycle",
- "unicode": "1F3CD",
- "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62"
- },
- {
- "name": "motorway",
- "unicode": "1F6E3",
+ "motorway": {
+ "category": "travel",
+ "moji": "🛣",
+ "unicodeVersion": "7.0",
"digest": "148c3c13c7c4565453d16e504e0d4b8d007e4f2cad1ab56b1b51fefe39162d17"
},
- {
- "name": "mount_fuji",
- "unicode": "1F5FB",
+ "mount_fuji": {
+ "category": "travel",
+ "moji": "🗻",
+ "unicodeVersion": "6.0",
"digest": "f8093b9dba62b22c6c88f137be88b2fd3971c560714db15ec053cf697a3820bc"
},
- {
- "name": "mountain",
- "unicode": "26F0",
+ "mountain": {
+ "category": "travel",
+ "moji": "⛰",
+ "unicodeVersion": "5.2",
"digest": "07423804ad79da68f140948d29df193f5d5343b7b2c23758c086697c4d3a50da"
},
- {
- "name": "mountain_bicyclist",
- "unicode": "1F6B5",
+ "mountain_bicyclist": {
+ "category": "activity",
+ "moji": "🚵",
+ "unicodeVersion": "6.0",
"digest": "91084b6c887cb7e34f3d7ec30656ecb82c36cc987f53a6c83ccb4c6f7950f96a"
},
- {
- "name": "mountain_bicyclist_tone1",
- "unicode": "1F6B5-1F3FB",
+ "mountain_bicyclist_tone1": {
+ "category": "activity",
+ "moji": "🚵🏻",
+ "unicodeVersion": "8.0",
"digest": "5d57fcfad61bca26c3e8965eb57602a1993a3117ebdda0f24569af730310ab6e"
},
- {
- "name": "mountain_bicyclist_tone2",
- "unicode": "1F6B5-1F3FC",
+ "mountain_bicyclist_tone2": {
+ "category": "activity",
+ "moji": "🚵🏼",
+ "unicodeVersion": "8.0",
"digest": "c0da7fb85d99aa01a665f64063cd7e2d994f8a16d3f6fbf52df5d471e771a98a"
},
- {
- "name": "mountain_bicyclist_tone3",
- "unicode": "1F6B5-1F3FD",
+ "mountain_bicyclist_tone3": {
+ "category": "activity",
+ "moji": "🚵🏽",
+ "unicodeVersion": "8.0",
"digest": "b099e7ee84eae44ebc99023fa06bdf37ffa0d69767c7c0163a89f7ced2a26765"
},
- {
- "name": "mountain_bicyclist_tone4",
- "unicode": "1F6B5-1F3FE",
+ "mountain_bicyclist_tone4": {
+ "category": "activity",
+ "moji": "🚵🏾",
+ "unicodeVersion": "8.0",
"digest": "9d09f7b3899ea44e736f237a161ef8d5170dccfa162a872c59532ceaf65ee007"
},
- {
- "name": "mountain_bicyclist_tone5",
- "unicode": "1F6B5-1F3FF",
+ "mountain_bicyclist_tone5": {
+ "category": "activity",
+ "moji": "🚵🏿",
+ "unicodeVersion": "8.0",
"digest": "71e374981d955056748a60c6d1820b45e9688a156b55318b4ea54a3a67ca801c"
},
- {
- "name": "mountain_cableway",
- "unicode": "1F6A0",
+ "mountain_cableway": {
+ "category": "travel",
+ "moji": "🚠",
+ "unicodeVersion": "6.0",
"digest": "e261c3292758b1c0063c5a0d0c7f5c9803306d2265e08677027e1210506ced94"
},
- {
- "name": "mountain_railway",
- "unicode": "1F69E",
+ "mountain_railway": {
+ "category": "travel",
+ "moji": "🚞",
+ "unicodeVersion": "6.0",
"digest": "b0987f8f391b3cbc7a56b9b8945ebfca240e01d12f8fd163877ebebe51d6b277"
},
- {
- "name": "mountain_snow",
- "unicode": "1F3D4",
- "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
- },
- {
- "name": "snow_capped_mountain",
- "unicode": "1F3D4",
+ "mountain_snow": {
+ "category": "travel",
+ "moji": "🏔",
+ "unicodeVersion": "7.0",
"digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189"
},
- {
- "name": "mouse",
- "unicode": "1F42D",
+ "mouse": {
+ "category": "nature",
+ "moji": "🐭",
+ "unicodeVersion": "6.0",
"digest": "007dd108507b45224f7a1fad3c1de6ecc75f38d71fc142744611eb13555f5eff"
},
- {
- "name": "mouse2",
- "unicode": "1F401",
+ "mouse2": {
+ "category": "nature",
+ "moji": "🐁",
+ "unicodeVersion": "6.0",
"digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d"
},
- {
- "name": "mouse_three_button",
- "unicode": "1F5B1",
+ "mouse_three_button": {
+ "category": "objects",
+ "moji": "🖱",
+ "unicodeVersion": "7.0",
"digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
},
- {
- "name": "three_button_mouse",
- "unicode": "1F5B1",
- "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
- },
- {
- "name": "movie_camera",
- "unicode": "1F3A5",
+ "movie_camera": {
+ "category": "objects",
+ "moji": "🎥",
+ "unicodeVersion": "6.0",
"digest": "f7e285eda35b4431c07951e071643ddc34147cd76640e0d516fbfd11208346e9"
},
- {
- "name": "moyai",
- "unicode": "1F5FF",
+ "moyai": {
+ "category": "objects",
+ "moji": "🗿",
+ "unicodeVersion": "6.0",
"digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb"
},
- {
- "name": "mrs_claus",
- "unicode": "1F936",
- "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
- },
- {
- "name": "mother_christmas",
- "unicode": "1F936",
+ "mrs_claus": {
+ "category": "people",
+ "moji": "🤶",
+ "unicodeVersion": "9.0",
"digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
},
- {
- "name": "mrs_claus_tone1",
- "unicode": "1F936-1F3FB",
+ "mrs_claus_tone1": {
+ "category": "people",
+ "moji": "🤶🏻",
+ "unicodeVersion": "9.0",
"digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
},
- {
- "name": "mother_christmas_tone1",
- "unicode": "1F936-1F3FB",
- "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
- },
- {
- "name": "mrs_claus_tone2",
- "unicode": "1F936-1F3FC",
+ "mrs_claus_tone2": {
+ "category": "people",
+ "moji": "🤶🏼",
+ "unicodeVersion": "9.0",
"digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
},
- {
- "name": "mother_christmas_tone2",
- "unicode": "1F936-1F3FC",
- "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
- },
- {
- "name": "mrs_claus_tone3",
- "unicode": "1F936-1F3FD",
+ "mrs_claus_tone3": {
+ "category": "people",
+ "moji": "🤶🏽",
+ "unicodeVersion": "9.0",
"digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
},
- {
- "name": "mother_christmas_tone3",
- "unicode": "1F936-1F3FD",
- "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
- },
- {
- "name": "mrs_claus_tone4",
- "unicode": "1F936-1F3FE",
- "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
- },
- {
- "name": "mother_christmas_tone4",
- "unicode": "1F936-1F3FE",
+ "mrs_claus_tone4": {
+ "category": "people",
+ "moji": "🤶🏾",
+ "unicodeVersion": "9.0",
"digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
},
- {
- "name": "mrs_claus_tone5",
- "unicode": "1F936-1F3FF",
+ "mrs_claus_tone5": {
+ "category": "people",
+ "moji": "🤶🏿",
+ "unicodeVersion": "9.0",
"digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
},
- {
- "name": "mother_christmas_tone5",
- "unicode": "1F936-1F3FF",
- "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
- },
- {
- "name": "muscle",
- "unicode": "1F4AA",
+ "muscle": {
+ "category": "people",
+ "moji": "💪",
+ "unicodeVersion": "6.0",
"digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba"
},
- {
- "name": "muscle_tone1",
- "unicode": "1F4AA-1F3FB",
+ "muscle_tone1": {
+ "category": "people",
+ "moji": "💪🏻",
+ "unicodeVersion": "8.0",
"digest": "4a2fa226a05bb847b62cdd163eb6c2d514d3c2330a727991cf550c0d32b0e818"
},
- {
- "name": "muscle_tone2",
- "unicode": "1F4AA-1F3FC",
+ "muscle_tone2": {
+ "category": "people",
+ "moji": "💪🏼",
+ "unicodeVersion": "8.0",
"digest": "a8d5ecce335c782ca5f5e55763c06cfefa1c16c24cd6602237cf125d4ff95e47"
},
- {
- "name": "muscle_tone3",
- "unicode": "1F4AA-1F3FD",
+ "muscle_tone3": {
+ "category": "people",
+ "moji": "💪🏽",
+ "unicodeVersion": "8.0",
"digest": "070354b443faec3969663b770545fc4cf5ec75148557b2b9d6fc82ab22b43bd1"
},
- {
- "name": "muscle_tone4",
- "unicode": "1F4AA-1F3FE",
+ "muscle_tone4": {
+ "category": "people",
+ "moji": "💪🏾",
+ "unicodeVersion": "8.0",
"digest": "8eafcdb6a607aeafa673c257df0d2a1b20f00fc0868d811babcbe784490a0dd3"
},
- {
- "name": "muscle_tone5",
- "unicode": "1F4AA-1F3FF",
+ "muscle_tone5": {
+ "category": "people",
+ "moji": "💪🏿",
+ "unicodeVersion": "8.0",
"digest": "85a1e2b5c89907694240e9c5b9d876a741fa7ba38918c5718273e289cbc40efe"
},
- {
- "name": "mushroom",
- "unicode": "1F344",
+ "mushroom": {
+ "category": "nature",
+ "moji": "🍄",
+ "unicodeVersion": "6.0",
"digest": "aaca8cf7c5cfa4487b5fef365a231f98be4bbf041197fc022161bcc8ce6f57c8"
},
- {
- "name": "musical_keyboard",
- "unicode": "1F3B9",
+ "musical_keyboard": {
+ "category": "activity",
+ "moji": "🎹",
+ "unicodeVersion": "6.0",
"digest": "fb0a726728900377d76d94aac9c94dce29107e8e3f1dcb0599d95bce7169b492"
},
- {
- "name": "musical_note",
- "unicode": "1F3B5",
+ "musical_note": {
+ "category": "symbols",
+ "moji": "🎵",
+ "unicodeVersion": "6.0",
"digest": "41288e79b4070bb980281d0e0d1c14d8b144b4aedb2eaadb9f2bebcb4ef892b4"
},
- {
- "name": "musical_score",
- "unicode": "1F3BC",
+ "musical_score": {
+ "category": "activity",
+ "moji": "🎼",
+ "unicodeVersion": "6.0",
"digest": "f0f91b9fa4a2bff7a5a1a11afa6f31cfe7e5fa8b0d6f3cce904b781a28ed0277"
},
- {
- "name": "mute",
- "unicode": "1F507",
+ "mute": {
+ "category": "symbols",
+ "moji": "🔇",
+ "unicodeVersion": "6.0",
"digest": "def277da49d744b55c7cdde269a15aa05315898f615e721ee7e9205d7b8030d6"
},
- {
- "name": "nail_care",
- "unicode": "1F485",
+ "nail_care": {
+ "category": "people",
+ "moji": "💅",
+ "unicodeVersion": "6.0",
"digest": "48b33b1dbbd25b4f34ab2ca07bb99ddaaaa741990142c5623310f76b78c076f9"
},
- {
- "name": "nail_care_tone1",
- "unicode": "1F485-1F3FB",
+ "nail_care_tone1": {
+ "category": "people",
+ "moji": "💅🏻",
+ "unicodeVersion": "8.0",
"digest": "a9ac92a34f407e7dd7c71377e6275e66657f7f42e4b911c540d1a66a02d92ac5"
},
- {
- "name": "nail_care_tone2",
- "unicode": "1F485-1F3FC",
+ "nail_care_tone2": {
+ "category": "people",
+ "moji": "💅🏼",
+ "unicodeVersion": "8.0",
"digest": "f295ec85980aaa75818fad619c3d25042146ecbbf361db9e9bb96e7bc202bc73"
},
- {
- "name": "nail_care_tone3",
- "unicode": "1F485-1F3FD",
+ "nail_care_tone3": {
+ "category": "people",
+ "moji": "💅🏽",
+ "unicodeVersion": "8.0",
"digest": "02ec373052a250977298bae85262177910126cc10de9480f1afa328ac2f65a95"
},
- {
- "name": "nail_care_tone4",
- "unicode": "1F485-1F3FE",
+ "nail_care_tone4": {
+ "category": "people",
+ "moji": "💅🏾",
+ "unicodeVersion": "8.0",
"digest": "f3d95390ab59caedfda66122bbd0acf3aabedc142fc48352d68900766a7e6f5c"
},
- {
- "name": "nail_care_tone5",
- "unicode": "1F485-1F3FF",
+ "nail_care_tone5": {
+ "category": "people",
+ "moji": "💅🏿",
+ "unicodeVersion": "8.0",
"digest": "009423c97f2aafd24fb8c7c485c58b30bbf9ae6797cc14b80d472b207327b518"
},
- {
- "name": "name_badge",
- "unicode": "1F4DB",
+ "name_badge": {
+ "category": "symbols",
+ "moji": "📛",
+ "unicodeVersion": "6.0",
"digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628"
},
- {
- "name": "nauseated_face",
- "unicode": "1F922",
- "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
- },
- {
- "name": "sick",
- "unicode": "1F922",
+ "nauseated_face": {
+ "category": "people",
+ "moji": "🤢",
+ "unicodeVersion": "9.0",
"digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
},
- {
- "name": "necktie",
- "unicode": "1F454",
+ "necktie": {
+ "category": "people",
+ "moji": "👔",
+ "unicodeVersion": "6.0",
"digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68"
},
- {
- "name": "negative_squared_cross_mark",
- "unicode": "274E",
+ "negative_squared_cross_mark": {
+ "category": "symbols",
+ "moji": "❎",
+ "unicodeVersion": "6.0",
"digest": "1cdaf4abc9adafa089c91c2e33a24e9e647aea0f857e767941a899a16ec53b74"
},
- {
- "name": "nerd",
- "unicode": "1F913",
- "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
- },
- {
- "name": "nerd_face",
- "unicode": "1F913",
+ "nerd": {
+ "category": "people",
+ "moji": "🤓",
+ "unicodeVersion": "8.0",
"digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
},
- {
- "name": "neutral_face",
- "unicode": "1F610",
+ "neutral_face": {
+ "category": "people",
+ "moji": "😐",
+ "unicodeVersion": "6.0",
"digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e"
},
- {
- "name": "new",
- "unicode": "1F195",
+ "new": {
+ "category": "symbols",
+ "moji": "🆕",
+ "unicodeVersion": "6.0",
"digest": "e20bc3e9f40726afd0cfb7268d02f1e1a07343364fd08b252d59f38de067bf06"
},
- {
- "name": "new_moon",
- "unicode": "1F311",
+ "new_moon": {
+ "category": "nature",
+ "moji": "🌑",
+ "unicodeVersion": "6.0",
"digest": "dbfc5dcae34b45f15ff767e297cba3a12cb83f3b542db8cfc8dbd9669e0df46c"
},
- {
- "name": "new_moon_with_face",
- "unicode": "1F31A",
+ "new_moon_with_face": {
+ "category": "nature",
+ "moji": "🌚",
+ "unicodeVersion": "6.0",
"digest": "c66d347d2222ac8d77d323a07699aff6b168328648db4f885b1ed0e2831fd59b"
},
- {
- "name": "newspaper",
- "unicode": "1F4F0",
+ "newspaper": {
+ "category": "objects",
+ "moji": "📰",
+ "unicodeVersion": "6.0",
"digest": "c05e986d9cdac11afa30c6a21a72572ddf50fc64e87ae0c4e0ad57ffe70acc5c"
},
- {
- "name": "newspaper2",
- "unicode": "1F5DE",
- "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
- },
- {
- "name": "rolled_up_newspaper",
- "unicode": "1F5DE",
+ "newspaper2": {
+ "category": "objects",
+ "moji": "🗞",
+ "unicodeVersion": "7.0",
"digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d"
},
- {
- "name": "ng",
- "unicode": "1F196",
+ "ng": {
+ "category": "symbols",
+ "moji": "🆖",
+ "unicodeVersion": "6.0",
"digest": "34d5a11c70f48ea719e602908534f446b192622e775d4160f0e1ec52c342a35c"
},
- {
- "name": "night_with_stars",
- "unicode": "1F303",
+ "night_with_stars": {
+ "category": "travel",
+ "moji": "🌃",
+ "unicodeVersion": "6.0",
"digest": "39d9c079be80ee6ce1667531be528a2aa7f8bd46c7b6c2a6ee279d9a207c84a4"
},
- {
- "name": "nine",
- "unicode": "0039-20E3",
+ "nine": {
+ "category": "symbols",
+ "moji": "9️⃣",
+ "unicodeVersion": "3.0",
"digest": "8bb40750eda8506ef877c9a3b8e2039d26f20eef345742f635740574a7e8daa6"
},
- {
- "name": "no_bell",
- "unicode": "1F515",
+ "no_bell": {
+ "category": "symbols",
+ "moji": "🔕",
+ "unicodeVersion": "6.0",
"digest": "6542a9a5656c79c153f8c37f12d48f677c89b02ed0989ae37fa5e51ce6895422"
},
- {
- "name": "no_bicycles",
- "unicode": "1F6B3",
+ "no_bicycles": {
+ "category": "symbols",
+ "moji": "🚳",
+ "unicodeVersion": "6.0",
"digest": "af71c183545da2ff4c05609f9d572edb64b63ccba7c6a4b208d271558aa92b0a"
},
- {
- "name": "no_entry",
- "unicode": "26D4",
+ "no_entry": {
+ "category": "symbols",
+ "moji": "⛔",
+ "unicodeVersion": "5.2",
"digest": "dc0bac1ed9ab8e9af143f0fce5043fe68f7f46bd80856cdec95d20c3999b637d"
},
- {
- "name": "no_entry_sign",
- "unicode": "1F6AB",
+ "no_entry_sign": {
+ "category": "symbols",
+ "moji": "🚫",
+ "unicodeVersion": "6.0",
"digest": "2c1fceef23b62effca68e0e087b8f020125d25b98d61492b1540055d1914fdc3"
},
- {
- "name": "no_good",
- "unicode": "1F645",
+ "no_good": {
+ "category": "people",
+ "moji": "🙅",
+ "unicodeVersion": "6.0",
"digest": "6eb970b104389be5d18657d7c04be5149958c26855c52ea68574af852c5f85c4"
},
- {
- "name": "no_good_tone1",
- "unicode": "1F645-1F3FB",
+ "no_good_tone1": {
+ "category": "people",
+ "moji": "🙅🏻",
+ "unicodeVersion": "8.0",
"digest": "c20a24a1e536240b4dcf90ecb530796de621d7ba1fb9e3fa0f849d048c509c03"
},
- {
- "name": "no_good_tone2",
- "unicode": "1F645-1F3FC",
+ "no_good_tone2": {
+ "category": "people",
+ "moji": "🙅🏼",
+ "unicodeVersion": "8.0",
"digest": "f31a4628c1f2e6a39288fda8eb19c9ec89983e3726e17a09384d9ecc13ef0b4c"
},
- {
- "name": "no_good_tone3",
- "unicode": "1F645-1F3FD",
+ "no_good_tone3": {
+ "category": "people",
+ "moji": "🙅🏽",
+ "unicodeVersion": "8.0",
"digest": "959dec1bfdaf37b20a86ab2bcbdbacd3179c87b163042377d966eab47564c0fb"
},
- {
- "name": "no_good_tone4",
- "unicode": "1F645-1F3FE",
+ "no_good_tone4": {
+ "category": "people",
+ "moji": "🙅🏾",
+ "unicodeVersion": "8.0",
"digest": "efd931f0080adf2e04129c83a8b24fda0ae7a9fa7c4b463686c0b99023620db8"
},
- {
- "name": "no_good_tone5",
- "unicode": "1F645-1F3FF",
+ "no_good_tone5": {
+ "category": "people",
+ "moji": "🙅🏿",
+ "unicodeVersion": "8.0",
"digest": "f35df2b26af9baef47c1f8cc97a1b28a58aa7fcb2a13fdac7b2d9189f1e40105"
},
- {
- "name": "no_mobile_phones",
- "unicode": "1F4F5",
+ "no_mobile_phones": {
+ "category": "symbols",
+ "moji": "📵",
+ "unicodeVersion": "6.0",
"digest": "a472decd6ac7f9777961c09e00458746b2c04965585e3bee4556be3968e55bcd"
},
- {
- "name": "no_mouth",
- "unicode": "1F636",
+ "no_mouth": {
+ "category": "people",
+ "moji": "😶",
+ "unicodeVersion": "6.0",
"digest": "72dda8b1e3ad4b05d9b095f9bd05e95d7ba013906c68914976a4554e8edf5866"
},
- {
- "name": "no_pedestrians",
- "unicode": "1F6B7",
+ "no_pedestrians": {
+ "category": "symbols",
+ "moji": "🚷",
+ "unicodeVersion": "6.0",
"digest": "062b4a71b338fe09775e465bfba8ac04efbb3640330e8cabe88f3af62b0f4225"
},
- {
- "name": "no_smoking",
- "unicode": "1F6AD",
+ "no_smoking": {
+ "category": "symbols",
+ "moji": "🚭",
+ "unicodeVersion": "6.0",
"digest": "ae2ebb331f79f6074091c0ee9cd69fce16d5e12a131d18973fc05520097e14ee"
},
- {
- "name": "non-potable_water",
- "unicode": "1F6B1",
+ "non-potable_water": {
+ "category": "symbols",
+ "moji": "🚱",
+ "unicodeVersion": "6.0",
"digest": "32eba0a99b498133c2e4450036f768d3dccaaf5b50adc9ad988757adc777a6a1"
},
- {
- "name": "nose",
- "unicode": "1F443",
+ "nose": {
+ "category": "people",
+ "moji": "👃",
+ "unicodeVersion": "6.0",
"digest": "9f800e24658ea3cebe1144d5d808cf13a88261f1a7f1f81a10d03b3d9d00e541"
},
- {
- "name": "nose_tone1",
- "unicode": "1F443-1F3FB",
+ "nose_tone1": {
+ "category": "people",
+ "moji": "👃🏻",
+ "unicodeVersion": "8.0",
"digest": "a2d0af22284b1d264eb780943b8360f463996a5c9c9584b8473edf8d442d9173"
},
- {
- "name": "nose_tone2",
- "unicode": "1F443-1F3FC",
+ "nose_tone2": {
+ "category": "people",
+ "moji": "👃🏼",
+ "unicodeVersion": "8.0",
"digest": "244dcaa8540024cf521f29f36bd48f933bf82f4833e35e6fa0abf113022038f3"
},
- {
- "name": "nose_tone3",
- "unicode": "1F443-1F3FD",
+ "nose_tone3": {
+ "category": "people",
+ "moji": "👃🏽",
+ "unicodeVersion": "8.0",
"digest": "c935b64866f0d49da52035aa09f36ff56d238eb7f5b92205386451056e8ea74f"
},
- {
- "name": "nose_tone4",
- "unicode": "1F443-1F3FE",
+ "nose_tone4": {
+ "category": "people",
+ "moji": "👃🏾",
+ "unicodeVersion": "8.0",
"digest": "a87e95fd9319c49e66b6dea0e57319d0ed9921b8d94df037767bf3d5dc7c94f3"
},
- {
- "name": "nose_tone5",
- "unicode": "1F443-1F3FF",
+ "nose_tone5": {
+ "category": "people",
+ "moji": "👃🏿",
+ "unicodeVersion": "8.0",
"digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60"
},
- {
- "name": "notebook",
- "unicode": "1F4D3",
+ "notebook": {
+ "category": "objects",
+ "moji": "📓",
+ "unicodeVersion": "6.0",
"digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8"
},
- {
- "name": "notebook_with_decorative_cover",
- "unicode": "1F4D4",
+ "notebook_with_decorative_cover": {
+ "category": "objects",
+ "moji": "📔",
+ "unicodeVersion": "6.0",
"digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef"
},
- {
- "name": "notepad_spiral",
- "unicode": "1F5D2",
+ "notepad_spiral": {
+ "category": "objects",
+ "moji": "🗒",
+ "unicodeVersion": "7.0",
"digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
},
- {
- "name": "spiral_note_pad",
- "unicode": "1F5D2",
- "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
- },
- {
- "name": "notes",
- "unicode": "1F3B6",
+ "notes": {
+ "category": "symbols",
+ "moji": "🎶",
+ "unicodeVersion": "6.0",
"digest": "98467e0adc134d45676ef1c6c459e5853a9db50c8a6e91b6aec7d449aa737f48"
},
- {
- "name": "nut_and_bolt",
- "unicode": "1F529",
+ "nut_and_bolt": {
+ "category": "objects",
+ "moji": "🔩",
+ "unicodeVersion": "6.0",
"digest": "a77bd72f29a7302195dcec240174b15586de79e3204258e3fb401a6ea90563b3"
},
- {
- "name": "o",
- "unicode": "2B55",
+ "o": {
+ "category": "symbols",
+ "moji": "⭕",
+ "unicodeVersion": "5.2",
"digest": "2387e5fd9ae4c2972d40298d32319b8fa55c50dbfc1c04c5c36088213e6951dd"
},
- {
- "name": "o2",
- "unicode": "1F17E",
+ "o2": {
+ "category": "symbols",
+ "moji": "🅾",
+ "unicodeVersion": "6.0",
"digest": "6a9ccb0bf394e4d05ffda19327cee18f7b9ed80367fc7f41c93da9bb7efab0bf"
},
- {
- "name": "ocean",
- "unicode": "1F30A",
+ "ocean": {
+ "category": "nature",
+ "moji": "🌊",
+ "unicodeVersion": "6.0",
"digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e"
},
- {
- "name": "octagonal_sign",
- "unicode": "1F6D1",
- "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
- },
- {
- "name": "stop_sign",
- "unicode": "1F6D1",
+ "octagonal_sign": {
+ "category": "symbols",
+ "moji": "🛑",
+ "unicodeVersion": "9.0",
"digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
},
- {
- "name": "octopus",
- "unicode": "1F419",
+ "octopus": {
+ "category": "nature",
+ "moji": "🐙",
+ "unicodeVersion": "6.0",
"digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59"
},
- {
- "name": "oden",
- "unicode": "1F362",
+ "oden": {
+ "category": "food",
+ "moji": "🍢",
+ "unicodeVersion": "6.0",
"digest": "089974cb13a0bef6a245fc73029c5ed5153fd4caae0177b835f779e32200b8aa"
},
- {
- "name": "office",
- "unicode": "1F3E2",
+ "office": {
+ "category": "travel",
+ "moji": "🏢",
+ "unicodeVersion": "6.0",
"digest": "3633a2e91036362e273eef4e0cfbdbbb4cb1208afe2cfa110ebef7b78109a66f"
},
- {
- "name": "oil",
- "unicode": "1F6E2",
- "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
- },
- {
- "name": "oil_drum",
- "unicode": "1F6E2",
+ "oil": {
+ "category": "objects",
+ "moji": "🛢",
+ "unicodeVersion": "7.0",
"digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa"
},
- {
- "name": "ok",
- "unicode": "1F197",
+ "ok": {
+ "category": "symbols",
+ "moji": "🆗",
+ "unicodeVersion": "6.0",
"digest": "5f320f9b96e98a2f17ebe240daff9b9fd2ae0727cd6c8e4633b1744356e89365"
},
- {
- "name": "ok_hand",
- "unicode": "1F44C",
+ "ok_hand": {
+ "category": "people",
+ "moji": "👌",
+ "unicodeVersion": "6.0",
"digest": "d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d"
},
- {
- "name": "ok_hand_tone1",
- "unicode": "1F44C-1F3FB",
+ "ok_hand_tone1": {
+ "category": "people",
+ "moji": "👌🏻",
+ "unicodeVersion": "8.0",
"digest": "ef1508efcf483b09807554fe0e451c2948224f9deb85463e8e0dad6875b54012"
},
- {
- "name": "ok_hand_tone2",
- "unicode": "1F44C-1F3FC",
+ "ok_hand_tone2": {
+ "category": "people",
+ "moji": "👌🏼",
+ "unicodeVersion": "8.0",
"digest": "1215a101a082fd8e04c5d2f7e3c59d0f480cb0bedd79aeab5d36676bfe760088"
},
- {
- "name": "ok_hand_tone3",
- "unicode": "1F44C-1F3FD",
+ "ok_hand_tone3": {
+ "category": "people",
+ "moji": "👌🏽",
+ "unicodeVersion": "8.0",
"digest": "6fe0ed9fb42e86bb2bed4cb37b2acacacda1471fb1ee845ad55e54fb0897fbf4"
},
- {
- "name": "ok_hand_tone4",
- "unicode": "1F44C-1F3FE",
+ "ok_hand_tone4": {
+ "category": "people",
+ "moji": "👌🏾",
+ "unicodeVersion": "8.0",
"digest": "bfb9041c49d95e901a667264abaf9b398f6c4aa8b52bf5191c122db20c13c020"
},
- {
- "name": "ok_hand_tone5",
- "unicode": "1F44C-1F3FF",
+ "ok_hand_tone5": {
+ "category": "people",
+ "moji": "👌🏿",
+ "unicodeVersion": "8.0",
"digest": "1c218dc04d698da2cbdd7bea1ca3f845f9b386e967b7247c52f4b0f6ec8f5320"
},
- {
- "name": "ok_woman",
- "unicode": "1F646",
+ "ok_woman": {
+ "category": "people",
+ "moji": "🙆",
+ "unicodeVersion": "6.0",
"digest": "3f8bd4ce2c4497155d697e5a71ebdc9339f65633d07fa9a7903e1bd76cfa4ba1"
},
- {
- "name": "ok_woman_tone1",
- "unicode": "1F646-1F3FB",
+ "ok_woman_tone1": {
+ "category": "people",
+ "moji": "🙆🏻",
+ "unicodeVersion": "8.0",
"digest": "1660cd904ccd2ecdc6f4ba00527f7d4ec8c33f3c6183344616f97badae4c3730"
},
- {
- "name": "ok_woman_tone2",
- "unicode": "1F646-1F3FC",
+ "ok_woman_tone2": {
+ "category": "people",
+ "moji": "🙆🏼",
+ "unicodeVersion": "8.0",
"digest": "7ba5fddd1e141424fac6778894dfc5af28e125839c58937c69496f99cd2c4002"
},
- {
- "name": "ok_woman_tone3",
- "unicode": "1F646-1F3FD",
+ "ok_woman_tone3": {
+ "category": "people",
+ "moji": "🙆🏽",
+ "unicodeVersion": "8.0",
"digest": "1d972b8377c52f598406f59ab1e5be41aaf8f027e1fefba3deda66312ccd6a9b"
},
- {
- "name": "ok_woman_tone4",
- "unicode": "1F646-1F3FE",
+ "ok_woman_tone4": {
+ "category": "people",
+ "moji": "🙆🏾",
+ "unicodeVersion": "8.0",
"digest": "a176328d8f53503aa743448968afd21d72ffd3510555526a3fb38d6b30ee7c15"
},
- {
- "name": "ok_woman_tone5",
- "unicode": "1F646-1F3FF",
+ "ok_woman_tone5": {
+ "category": "people",
+ "moji": "🙆🏿",
+ "unicodeVersion": "8.0",
"digest": "13cfc1b589c57e81f768ee07a14b737cafc71407a7eb0956728b2ec4b1df14c4"
},
- {
- "name": "older_man",
- "unicode": "1F474",
+ "older_man": {
+ "category": "people",
+ "moji": "👴",
+ "unicodeVersion": "6.0",
"digest": "4c0462b199bf26181c9e4d2d4cb878a32b0294566941212efc67362d0645f948"
},
- {
- "name": "older_man_tone1",
- "unicode": "1F474-1F3FB",
+ "older_man_tone1": {
+ "category": "people",
+ "moji": "👴🏻",
+ "unicodeVersion": "8.0",
"digest": "99baa083f78cb01166d0a928d0b53682be14be04c29fc17bef14aac1a73a61e6"
},
- {
- "name": "older_man_tone2",
- "unicode": "1F474-1F3FC",
+ "older_man_tone2": {
+ "category": "people",
+ "moji": "👴🏼",
+ "unicodeVersion": "8.0",
"digest": "5b4ce713e8820ba517fe92c25f3b93e6a6bf3704d1f982c461d5f31fc02b9d3d"
},
- {
- "name": "older_man_tone3",
- "unicode": "1F474-1F3FD",
+ "older_man_tone3": {
+ "category": "people",
+ "moji": "👴🏽",
+ "unicodeVersion": "8.0",
"digest": "0eff72b3226c3a703c635798ee84129a695c896fa011fe1adbc105312eecc083"
},
- {
- "name": "older_man_tone4",
- "unicode": "1F474-1F3FE",
+ "older_man_tone4": {
+ "category": "people",
+ "moji": "👴🏾",
+ "unicodeVersion": "8.0",
"digest": "ad9ba82b0c5d3b171b0639ee4265370dbddff5e0eeb70729db122659bb8c8f84"
},
- {
- "name": "older_man_tone5",
- "unicode": "1F474-1F3FF",
+ "older_man_tone5": {
+ "category": "people",
+ "moji": "👴🏿",
+ "unicodeVersion": "8.0",
"digest": "5eb0a7467cc40e75752e11fd5126b275863dc037557a0d0d3b24b681e00c2386"
},
- {
- "name": "older_woman",
- "unicode": "1F475",
- "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
- },
- {
- "name": "grandma",
- "unicode": "1F475",
+ "older_woman": {
+ "category": "people",
+ "moji": "👵",
+ "unicodeVersion": "6.0",
"digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6"
},
- {
- "name": "older_woman_tone1",
- "unicode": "1F475-1F3FB",
- "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
- },
- {
- "name": "grandma_tone1",
- "unicode": "1F475-1F3FB",
+ "older_woman_tone1": {
+ "category": "people",
+ "moji": "👵🏻",
+ "unicodeVersion": "8.0",
"digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62"
},
- {
- "name": "older_woman_tone2",
- "unicode": "1F475-1F3FC",
- "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
- },
- {
- "name": "grandma_tone2",
- "unicode": "1F475-1F3FC",
+ "older_woman_tone2": {
+ "category": "people",
+ "moji": "👵🏼",
+ "unicodeVersion": "8.0",
"digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940"
},
- {
- "name": "older_woman_tone3",
- "unicode": "1F475-1F3FD",
- "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
- },
- {
- "name": "grandma_tone3",
- "unicode": "1F475-1F3FD",
+ "older_woman_tone3": {
+ "category": "people",
+ "moji": "👵🏽",
+ "unicodeVersion": "8.0",
"digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67"
},
- {
- "name": "older_woman_tone4",
- "unicode": "1F475-1F3FE",
- "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
- },
- {
- "name": "grandma_tone4",
- "unicode": "1F475-1F3FE",
+ "older_woman_tone4": {
+ "category": "people",
+ "moji": "👵🏾",
+ "unicodeVersion": "8.0",
"digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44"
},
- {
- "name": "older_woman_tone5",
- "unicode": "1F475-1F3FF",
+ "older_woman_tone5": {
+ "category": "people",
+ "moji": "👵🏿",
+ "unicodeVersion": "8.0",
"digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
},
- {
- "name": "grandma_tone5",
- "unicode": "1F475-1F3FF",
- "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275"
- },
- {
- "name": "om_symbol",
- "unicode": "1F549",
+ "om_symbol": {
+ "category": "symbols",
+ "moji": "🕉",
+ "unicodeVersion": "7.0",
"digest": "5ead73bea546ba9ba6da522f7280cc289c75ff5467742bdba31f92d0e1b3f4e6"
},
- {
- "name": "on",
- "unicode": "1F51B",
+ "on": {
+ "category": "symbols",
+ "moji": "🔛",
+ "unicodeVersion": "6.0",
"digest": "9cc61a6b31a30c32dab594191bf23f91e341c4105384ab22158a6d43e6364631"
},
- {
- "name": "oncoming_automobile",
- "unicode": "1F698",
+ "oncoming_automobile": {
+ "category": "travel",
+ "moji": "🚘",
+ "unicodeVersion": "6.0",
"digest": "557c9cacdc3f95215d4f7a6f097a2baa7c007cb9c519492a6717077af4ca6b56"
},
- {
- "name": "oncoming_bus",
- "unicode": "1F68D",
+ "oncoming_bus": {
+ "category": "travel",
+ "moji": "🚍",
+ "unicodeVersion": "6.0",
"digest": "059f28ce6bfb337e107db5982cbd2004844450ef20b4a54b9ca3cb738360ab05"
},
- {
- "name": "oncoming_police_car",
- "unicode": "1F694",
+ "oncoming_police_car": {
+ "category": "travel",
+ "moji": "🚔",
+ "unicodeVersion": "6.0",
"digest": "aee79306a0d129cfc1980f58db80391eb46d2d7d5f814bf431414dc7680cab72"
},
- {
- "name": "oncoming_taxi",
- "unicode": "1F696",
+ "oncoming_taxi": {
+ "category": "travel",
+ "moji": "🚖",
+ "unicodeVersion": "6.0",
"digest": "84351489fc86d980b8d3eb9ec4e81120fe700b3ac01346daebe2b7aeb9607a55"
},
- {
- "name": "one",
- "unicode": "0031-20E3",
+ "one": {
+ "category": "symbols",
+ "moji": "1️⃣",
+ "unicodeVersion": "3.0",
"digest": "d5d3fff04e68a114ff6464ee06fc831f3f381713045165f62a88d5e8215c195b"
},
- {
- "name": "open_file_folder",
- "unicode": "1F4C2",
+ "open_file_folder": {
+ "category": "objects",
+ "moji": "📂",
+ "unicodeVersion": "6.0",
"digest": "96cfc322ee4903ae8cec07604811742245fd7d14f00bb70276d39d29c48bed28"
},
- {
- "name": "open_hands",
- "unicode": "1F450",
+ "open_hands": {
+ "category": "people",
+ "moji": "👐",
+ "unicodeVersion": "6.0",
"digest": "a6c131da2040b48103cea14f280e728675da50fa448d2b3f3438fcbb5bf5596a"
},
- {
- "name": "open_hands_tone1",
- "unicode": "1F450-1F3FB",
+ "open_hands_tone1": {
+ "category": "people",
+ "moji": "👐🏻",
+ "unicodeVersion": "8.0",
"digest": "867128dff2fa9b860c10c6b792f989f0c057928783696062378f834c0ef89d85"
},
- {
- "name": "open_hands_tone2",
- "unicode": "1F450-1F3FC",
+ "open_hands_tone2": {
+ "category": "people",
+ "moji": "👐🏼",
+ "unicodeVersion": "8.0",
"digest": "487ff2745b03d49bb3b1d0acd86ba530fd8cc3f467ca3fa504f88f0ef1cbbc01"
},
- {
- "name": "open_hands_tone3",
- "unicode": "1F450-1F3FD",
+ "open_hands_tone3": {
+ "category": "people",
+ "moji": "👐🏽",
+ "unicodeVersion": "8.0",
"digest": "cb8cddc8b8661f874ac9478289d16cc41406b947bb87f3363df518a588a53e16"
},
- {
- "name": "open_hands_tone4",
- "unicode": "1F450-1F3FE",
+ "open_hands_tone4": {
+ "category": "people",
+ "moji": "👐🏾",
+ "unicodeVersion": "8.0",
"digest": "17dcc2c07230846a769f3c79ce618a757c88b9b58c95c6c5b2d7f968814d447d"
},
- {
- "name": "open_hands_tone5",
- "unicode": "1F450-1F3FF",
+ "open_hands_tone5": {
+ "category": "people",
+ "moji": "👐🏿",
+ "unicodeVersion": "8.0",
"digest": "36b2493d67c84cea4f3f85a3088c6abcfd35cf99f7aeaeedfafa420ee878e3d2"
},
- {
- "name": "open_mouth",
- "unicode": "1F62E",
+ "open_mouth": {
+ "category": "people",
+ "moji": "😮",
+ "unicodeVersion": "6.1",
"digest": "1906c5100ae0c8326ca5c4f9422976958a38dadd8d77724d68538a25d9623035"
},
- {
- "name": "ophiuchus",
- "unicode": "26CE",
+ "ophiuchus": {
+ "category": "symbols",
+ "moji": "⛎",
+ "unicodeVersion": "6.0",
"digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b"
},
- {
- "name": "orange_book",
- "unicode": "1F4D9",
+ "orange_book": {
+ "category": "objects",
+ "moji": "📙",
+ "unicodeVersion": "6.0",
"digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf"
},
- {
- "name": "orthodox_cross",
- "unicode": "2626",
+ "orthodox_cross": {
+ "category": "symbols",
+ "moji": "☦",
+ "unicodeVersion": "1.1",
"digest": "c16372102f0169dd6d32eb2b27a633aaee74e4e0fddcf723c15ad97f9dc6075c"
},
- {
- "name": "outbox_tray",
- "unicode": "1F4E4",
+ "outbox_tray": {
+ "category": "objects",
+ "moji": "📤",
+ "unicodeVersion": "6.0",
"digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf"
},
- {
- "name": "owl",
- "unicode": "1F989",
+ "owl": {
+ "category": "nature",
+ "moji": "🦉",
+ "unicodeVersion": "9.0",
"digest": "f62ec1ad23ad9038966eea8d8b79660ac212f291af2e89bcdb0fdc683caf41e5"
},
- {
- "name": "ox",
- "unicode": "1F402",
+ "ox": {
+ "category": "nature",
+ "moji": "🐂",
+ "unicodeVersion": "6.0",
"digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed"
},
- {
- "name": "package",
- "unicode": "1F4E6",
+ "package": {
+ "category": "objects",
+ "moji": "📦",
+ "unicodeVersion": "6.0",
"digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c"
},
- {
- "name": "page_facing_up",
- "unicode": "1F4C4",
+ "page_facing_up": {
+ "category": "objects",
+ "moji": "📄",
+ "unicodeVersion": "6.0",
"digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a"
},
- {
- "name": "page_with_curl",
- "unicode": "1F4C3",
+ "page_with_curl": {
+ "category": "objects",
+ "moji": "📃",
+ "unicodeVersion": "6.0",
"digest": "3d6257670189f841ad1fa45415c34feb2433b2cb35bb435c4ee122ce89b39669"
},
- {
- "name": "pager",
- "unicode": "1F4DF",
+ "pager": {
+ "category": "objects",
+ "moji": "📟",
+ "unicodeVersion": "6.0",
"digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12"
},
- {
- "name": "paintbrush",
- "unicode": "1F58C",
- "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
- },
- {
- "name": "lower_left_paintbrush",
- "unicode": "1F58C",
+ "paintbrush": {
+ "category": "objects",
+ "moji": "🖌",
+ "unicodeVersion": "7.0",
"digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
},
- {
- "name": "palm_tree",
- "unicode": "1F334",
+ "palm_tree": {
+ "category": "nature",
+ "moji": "🌴",
+ "unicodeVersion": "6.0",
"digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1"
},
- {
- "name": "pancakes",
- "unicode": "1F95E",
+ "pancakes": {
+ "category": "food",
+ "moji": "🥞",
+ "unicodeVersion": "9.0",
"digest": "5256b4832431e8a88555796b1a9726f12d909a26fb2bdc3a0abff76412c45903"
},
- {
- "name": "panda_face",
- "unicode": "1F43C",
+ "panda_face": {
+ "category": "nature",
+ "moji": "🐼",
+ "unicodeVersion": "6.0",
"digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b"
},
- {
- "name": "paperclip",
- "unicode": "1F4CE",
+ "paperclip": {
+ "category": "objects",
+ "moji": "📎",
+ "unicodeVersion": "6.0",
"digest": "d1e2ce94a12b7e8b7a9bba49e47ddc7432ec0288545d3b6817c7a499e806e3f0"
},
- {
- "name": "paperclips",
- "unicode": "1F587",
- "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
- },
- {
- "name": "linked_paperclips",
- "unicode": "1F587",
+ "paperclips": {
+ "category": "objects",
+ "moji": "🖇",
+ "unicodeVersion": "7.0",
"digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2"
},
- {
- "name": "park",
- "unicode": "1F3DE",
+ "park": {
+ "category": "travel",
+ "moji": "🏞",
+ "unicodeVersion": "7.0",
"digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
},
- {
- "name": "national_park",
- "unicode": "1F3DE",
- "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7"
- },
- {
- "name": "parking",
- "unicode": "1F17F",
+ "parking": {
+ "category": "symbols",
+ "moji": "🅿",
+ "unicodeVersion": "5.2",
"digest": "9f1da460a7dd58b26beab8cf701be2691fb812208fbc941c71daa35be1507c2f"
},
- {
- "name": "part_alternation_mark",
- "unicode": "303D",
+ "part_alternation_mark": {
+ "category": "symbols",
+ "moji": "〽",
+ "unicodeVersion": "3.2",
"digest": "956da19353bb38fd4dfe0ab5360679a9035d566858fb5de62887b85c75fb8eef"
},
- {
- "name": "partly_sunny",
- "unicode": "26C5",
+ "partly_sunny": {
+ "category": "nature",
+ "moji": "⛅",
+ "unicodeVersion": "5.2",
"digest": "8fb9a6d2caf9e0cce58447762f0dfd6aa0b581b2e83fea6411348e0cbc8cf3c4"
},
- {
- "name": "passport_control",
- "unicode": "1F6C2",
+ "passport_control": {
+ "category": "symbols",
+ "moji": "🛂",
+ "unicodeVersion": "6.0",
"digest": "d9be6eed2c90e1c89171c42d70a06485fdf86a4c68833371832cc1f6897fadd0"
},
- {
- "name": "pause_button",
- "unicode": "23F8",
- "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
- },
- {
- "name": "double_vertical_bar",
- "unicode": "23F8",
+ "pause_button": {
+ "category": "symbols",
+ "moji": "⏸",
+ "unicodeVersion": "7.0",
"digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203"
},
- {
- "name": "peace",
- "unicode": "262E",
+ "peace": {
+ "category": "symbols",
+ "moji": "☮",
+ "unicodeVersion": "1.1",
"digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
},
- {
- "name": "peace_symbol",
- "unicode": "262E",
- "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4"
- },
- {
- "name": "peach",
- "unicode": "1F351",
+ "peach": {
+ "category": "food",
+ "moji": "🍑",
+ "unicodeVersion": "6.0",
"digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311"
},
- {
- "name": "peanuts",
- "unicode": "1F95C",
- "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
- },
- {
- "name": "shelled_peanut",
- "unicode": "1F95C",
+ "peanuts": {
+ "category": "food",
+ "moji": "🥜",
+ "unicodeVersion": "9.0",
"digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
},
- {
- "name": "pear",
- "unicode": "1F350",
+ "pear": {
+ "category": "food",
+ "moji": "🍐",
+ "unicodeVersion": "6.0",
"digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948"
},
- {
- "name": "pen_ballpoint",
- "unicode": "1F58A",
+ "pen_ballpoint": {
+ "category": "objects",
+ "moji": "🖊",
+ "unicodeVersion": "7.0",
"digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
},
- {
- "name": "lower_left_ballpoint_pen",
- "unicode": "1F58A",
- "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876"
- },
- {
- "name": "pen_fountain",
- "unicode": "1F58B",
- "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
- },
- {
- "name": "lower_left_fountain_pen",
- "unicode": "1F58B",
+ "pen_fountain": {
+ "category": "objects",
+ "moji": "🖋",
+ "unicodeVersion": "7.0",
"digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626"
},
- {
- "name": "pencil",
- "unicode": "1F4DD",
+ "pencil": {
+ "category": "objects",
+ "moji": "📝",
+ "unicodeVersion": "6.0",
"digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
},
- {
- "name": "memo",
- "unicode": "1F4DD",
- "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c"
- },
- {
- "name": "pencil2",
- "unicode": "270F",
+ "pencil2": {
+ "category": "objects",
+ "moji": "✏",
+ "unicodeVersion": "1.1",
"digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0"
},
- {
- "name": "penguin",
- "unicode": "1F427",
+ "penguin": {
+ "category": "nature",
+ "moji": "🐧",
+ "unicodeVersion": "6.0",
"digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316"
},
- {
- "name": "pensive",
- "unicode": "1F614",
+ "pensive": {
+ "category": "people",
+ "moji": "😔",
+ "unicodeVersion": "6.0",
"digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2"
},
- {
- "name": "performing_arts",
- "unicode": "1F3AD",
+ "performing_arts": {
+ "category": "activity",
+ "moji": "🎭",
+ "unicodeVersion": "6.0",
"digest": "d7c7bc9213e308ca26286cbbd8012e656b0f9b00293758faf1bfccc4c5ceabed"
},
- {
- "name": "persevere",
- "unicode": "1F623",
+ "persevere": {
+ "category": "people",
+ "moji": "😣",
+ "unicodeVersion": "6.0",
"digest": "c361509c9b8663af19a02a1ffff61b1b0d0b4bd75d693ce3d406b0ca1bde1ca0"
},
- {
- "name": "person_frowning",
- "unicode": "1F64D",
+ "person_frowning": {
+ "category": "people",
+ "moji": "🙍",
+ "unicodeVersion": "6.0",
"digest": "b37be8bd95f21a6860ad3f171b8086125ab37331b382d87bcdb4cd684800546b"
},
- {
- "name": "person_frowning_tone1",
- "unicode": "1F64D-1F3FB",
+ "person_frowning_tone1": {
+ "category": "people",
+ "moji": "🙍🏻",
+ "unicodeVersion": "8.0",
"digest": "3d5e78a367f9673baed2a86bc11cf04fd44394aadb65291fa51ade8dca318427"
},
- {
- "name": "person_frowning_tone2",
- "unicode": "1F64D-1F3FC",
+ "person_frowning_tone2": {
+ "category": "people",
+ "moji": "🙍🏼",
+ "unicodeVersion": "8.0",
"digest": "7456c414c65ad6b6f11855f68a2eedc18113526f86862c4373202397cb1bed2c"
},
- {
- "name": "person_frowning_tone3",
- "unicode": "1F64D-1F3FD",
+ "person_frowning_tone3": {
+ "category": "people",
+ "moji": "🙍🏽",
+ "unicodeVersion": "8.0",
"digest": "c86cf2d6951f1e6a7c786a74caaf68a777cf00e88023e23849d4383f864ae437"
},
- {
- "name": "person_frowning_tone4",
- "unicode": "1F64D-1F3FE",
+ "person_frowning_tone4": {
+ "category": "people",
+ "moji": "🙍🏾",
+ "unicodeVersion": "8.0",
"digest": "944e96ced645ced8db6bb50120c7e37ed46b6960d595cbfe964c81803efa83aa"
},
- {
- "name": "person_frowning_tone5",
- "unicode": "1F64D-1F3FF",
+ "person_frowning_tone5": {
+ "category": "people",
+ "moji": "🙍🏿",
+ "unicodeVersion": "8.0",
"digest": "4bd0ea571be6ef9f0493784ef0d12d5e47bc2d6ac610fb42c450bf3d87fb2948"
},
- {
- "name": "person_with_blond_hair",
- "unicode": "1F471",
+ "person_with_blond_hair": {
+ "category": "people",
+ "moji": "👱",
+ "unicodeVersion": "6.0",
"digest": "a7f94ede2e43308108c2260d83fc10121dda09a67f94a0a840e6d7bba7fd5616"
},
- {
- "name": "person_with_blond_hair_tone1",
- "unicode": "1F471-1F3FB",
+ "person_with_blond_hair_tone1": {
+ "category": "people",
+ "moji": "👱🏻",
+ "unicodeVersion": "8.0",
"digest": "00a116357a7878554c83e5bade4bddfa9cfabf76a229efa19cbb58e0d216219c"
},
- {
- "name": "person_with_blond_hair_tone2",
- "unicode": "1F471-1F3FC",
+ "person_with_blond_hair_tone2": {
+ "category": "people",
+ "moji": "👱🏼",
+ "unicodeVersion": "8.0",
"digest": "df509ebe92ed3138b9d5bd4645eff4b13f77f714cf62bb949c59eff1adc00019"
},
- {
- "name": "person_with_blond_hair_tone3",
- "unicode": "1F471-1F3FD",
+ "person_with_blond_hair_tone3": {
+ "category": "people",
+ "moji": "👱🏽",
+ "unicodeVersion": "8.0",
"digest": "6f328513f440a0c8cd1dc44596a5028fd8f306bdaf57c1e6f3aa94a3aa262b3c"
},
- {
- "name": "person_with_blond_hair_tone4",
- "unicode": "1F471-1F3FE",
+ "person_with_blond_hair_tone4": {
+ "category": "people",
+ "moji": "👱🏾",
+ "unicodeVersion": "8.0",
"digest": "32df1a577815b009696643ad80d063cc97b35d54add6d4e5517fc936f6da9ee8"
},
- {
- "name": "person_with_blond_hair_tone5",
- "unicode": "1F471-1F3FF",
+ "person_with_blond_hair_tone5": {
+ "category": "people",
+ "moji": "👱🏿",
+ "unicodeVersion": "8.0",
"digest": "2e270bb39187d8e36a33f4aa4d6045308189595fafc157cf7993e82d7ce93442"
},
- {
- "name": "person_with_pouting_face",
- "unicode": "1F64E",
+ "person_with_pouting_face": {
+ "category": "people",
+ "moji": "🙎",
+ "unicodeVersion": "6.0",
"digest": "57e9a6e5f82121516dc189173f2a63b218f726cd51014e24a18c2bdfeeec3a0b"
},
- {
- "name": "person_with_pouting_face_tone1",
- "unicode": "1F64E-1F3FB",
+ "person_with_pouting_face_tone1": {
+ "category": "people",
+ "moji": "🙎🏻",
+ "unicodeVersion": "8.0",
"digest": "d10dadb1ac03fc2e221eff77b4c47935dc0b4fe897af3de30461e7226c3b4bbc"
},
- {
- "name": "person_with_pouting_face_tone2",
- "unicode": "1F64E-1F3FC",
+ "person_with_pouting_face_tone2": {
+ "category": "people",
+ "moji": "🙎🏼",
+ "unicodeVersion": "8.0",
"digest": "efface531537ab934b3b96985210a2dac88de812e82e804d6ec12174e536d1cc"
},
- {
- "name": "person_with_pouting_face_tone3",
- "unicode": "1F64E-1F3FD",
+ "person_with_pouting_face_tone3": {
+ "category": "people",
+ "moji": "🙎🏽",
+ "unicodeVersion": "8.0",
"digest": "7ff26ece237216b949bfa96d16bd12cfd248c6fd3e4ed89aa6c735c09eafaeff"
},
- {
- "name": "person_with_pouting_face_tone4",
- "unicode": "1F64E-1F3FE",
+ "person_with_pouting_face_tone4": {
+ "category": "people",
+ "moji": "🙎🏾",
+ "unicodeVersion": "8.0",
"digest": "045c04105df41d94ff4942133c7394e42ff35ef76c4ccb711497ab77ae6219f2"
},
- {
- "name": "person_with_pouting_face_tone5",
- "unicode": "1F64E-1F3FF",
+ "person_with_pouting_face_tone5": {
+ "category": "people",
+ "moji": "🙎🏿",
+ "unicodeVersion": "8.0",
"digest": "783ee37f146fcf61d38af5009f5823cf6526fe99ed891979f454016bce9dd4ba"
},
- {
- "name": "pick",
- "unicode": "26CF",
+ "pick": {
+ "category": "objects",
+ "moji": "⛏",
+ "unicodeVersion": "5.2",
"digest": "7f0ec5445b4d5c66cf46e2a7332946cce34bd70e9929ac7a119251a7f57f555d"
},
- {
- "name": "pig",
- "unicode": "1F437",
+ "pig": {
+ "category": "nature",
+ "moji": "🐷",
+ "unicodeVersion": "6.0",
"digest": "51362570ab36805c8f67622ee4543e38811f8abb20f732a1af2ffbff2d63d042"
},
- {
- "name": "pig2",
- "unicode": "1F416",
+ "pig2": {
+ "category": "nature",
+ "moji": "🐖",
+ "unicodeVersion": "6.0",
"digest": "67010e255f28061b9d9210bcdab6edc072642ad134122a1d0c7e3a6b1795a45b"
},
- {
- "name": "pig_nose",
- "unicode": "1F43D",
+ "pig_nose": {
+ "category": "nature",
+ "moji": "🐽",
+ "unicodeVersion": "6.0",
"digest": "0b21cac238bf4910939fbea9bed35552378c1b605a3867d7b85c1556dbda22a9"
},
- {
- "name": "pill",
- "unicode": "1F48A",
+ "pill": {
+ "category": "objects",
+ "moji": "💊",
+ "unicodeVersion": "6.0",
"digest": "cb00be361aaba6dbcf8da58bd20b76221dd75031362ecae99496b088ed413a7f"
},
- {
- "name": "pineapple",
- "unicode": "1F34D",
+ "pineapple": {
+ "category": "food",
+ "moji": "🍍",
+ "unicodeVersion": "6.0",
"digest": "621d4d4c52b59e566c2e29ed7845c8bd2d1da0946577527342097808d170dd70"
},
- {
- "name": "ping_pong",
- "unicode": "1F3D3",
- "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
- },
- {
- "name": "table_tennis",
- "unicode": "1F3D3",
+ "ping_pong": {
+ "category": "activity",
+ "moji": "🏓",
+ "unicodeVersion": "8.0",
"digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
},
- {
- "name": "pisces",
- "unicode": "2653",
+ "pisces": {
+ "category": "symbols",
+ "moji": "♓",
+ "unicodeVersion": "1.1",
"digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a"
},
- {
- "name": "pizza",
- "unicode": "1F355",
+ "pizza": {
+ "category": "food",
+ "moji": "🍕",
+ "unicodeVersion": "6.0",
"digest": "169bc6c1e1d7fdab1b8bf2eab0eeec4f9a7ae08b7b9b38f33b0b0c642e72053a"
},
- {
- "name": "place_of_worship",
- "unicode": "1F6D0",
+ "place_of_worship": {
+ "category": "symbols",
+ "moji": "🛐",
+ "unicodeVersion": "8.0",
"digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
},
- {
- "name": "worship_symbol",
- "unicode": "1F6D0",
- "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644"
- },
- {
- "name": "play_pause",
- "unicode": "23EF",
+ "play_pause": {
+ "category": "symbols",
+ "moji": "⏯",
+ "unicodeVersion": "6.0",
"digest": "af1498f34a3d6e0da8bbd26ebaa447e697e2df08c8eb255437cf7905c93f8c42"
},
- {
- "name": "point_down",
- "unicode": "1F447",
+ "point_down": {
+ "category": "people",
+ "moji": "👇",
+ "unicodeVersion": "6.0",
"digest": "4ecdb3f31c16dc38113b8854ec1a7884613b688a185ebdf967eab9a81018f76d"
},
- {
- "name": "point_down_tone1",
- "unicode": "1F447-1F3FB",
+ "point_down_tone1": {
+ "category": "people",
+ "moji": "👇🏻",
+ "unicodeVersion": "8.0",
"digest": "c74a7c94367cddbfa840542dc0924adeb0d108be0c7fde8c25fb95d69115d283"
},
- {
- "name": "point_down_tone2",
- "unicode": "1F447-1F3FC",
+ "point_down_tone2": {
+ "category": "people",
+ "moji": "👇🏼",
+ "unicodeVersion": "8.0",
"digest": "dc4bda0726d85418b974addb42738f437fbb9cf16e5815cdbab3859c4ada6cae"
},
- {
- "name": "point_down_tone3",
- "unicode": "1F447-1F3FD",
+ "point_down_tone3": {
+ "category": "people",
+ "moji": "👇🏽",
+ "unicodeVersion": "8.0",
"digest": "e460f81a501376d2f0ed1d45e358c5ed03ba049e8f466e4298afb4f3ca6d24dc"
},
- {
- "name": "point_down_tone4",
- "unicode": "1F447-1F3FE",
+ "point_down_tone4": {
+ "category": "people",
+ "moji": "👇🏾",
+ "unicodeVersion": "8.0",
"digest": "4bc91cd771f24e0f897a9d8b18f323fec9a82da0fc2429c4a7e4e6a9d885a0a3"
},
- {
- "name": "point_down_tone5",
- "unicode": "1F447-1F3FF",
+ "point_down_tone5": {
+ "category": "people",
+ "moji": "👇🏿",
+ "unicodeVersion": "8.0",
"digest": "7e47c6bc73250f36dc7ae1c1c09e7b41f30647b9d0ff703a53a75cc046b5057d"
},
- {
- "name": "point_left",
- "unicode": "1F448",
+ "point_left": {
+ "category": "people",
+ "moji": "👈",
+ "unicodeVersion": "6.0",
"digest": "b5a7e864a0016afbadb3bec41f51ecf8c4af73cc20462e1a08b357f90bca6879"
},
- {
- "name": "point_left_tone1",
- "unicode": "1F448-1F3FB",
+ "point_left_tone1": {
+ "category": "people",
+ "moji": "👈🏻",
+ "unicodeVersion": "8.0",
"digest": "9f1868272a10a2b738c065be5d30241643324550cfd47baf01c7a09060e66d31"
},
- {
- "name": "point_left_tone2",
- "unicode": "1F448-1F3FC",
+ "point_left_tone2": {
+ "category": "people",
+ "moji": "👈🏼",
+ "unicodeVersion": "8.0",
"digest": "bf0d58c68178a2c2c01d4a6235a1a66b90073cea170f9f6fe2668b6dd68424f7"
},
- {
- "name": "point_left_tone3",
- "unicode": "1F448-1F3FD",
+ "point_left_tone3": {
+ "category": "people",
+ "moji": "👈🏽",
+ "unicodeVersion": "8.0",
"digest": "34d28c97bc8f9d111d14e328153c4298fc32cf18e39e20aacaec17846645ed90"
},
- {
- "name": "point_left_tone4",
- "unicode": "1F448-1F3FE",
+ "point_left_tone4": {
+ "category": "people",
+ "moji": "👈🏾",
+ "unicodeVersion": "8.0",
"digest": "c40c8436316915d516c53bb1c98a469528cefd98baa719be7e748c4608cbbcc9"
},
- {
- "name": "point_left_tone5",
- "unicode": "1F448-1F3FF",
+ "point_left_tone5": {
+ "category": "people",
+ "moji": "👈🏿",
+ "unicodeVersion": "8.0",
"digest": "c410fe32e4ce0ded74845a54b86090e59e5820d457837b16e175b36cc71ecb46"
},
- {
- "name": "point_right",
- "unicode": "1F449",
+ "point_right": {
+ "category": "people",
+ "moji": "👉",
+ "unicodeVersion": "6.0",
"digest": "44d9251ab41f2f48c2250c44a47f92b3476a71f13fbbbfb637547db837fd5a49"
},
- {
- "name": "point_right_tone1",
- "unicode": "1F449-1F3FB",
+ "point_right_tone1": {
+ "category": "people",
+ "moji": "👉🏻",
+ "unicodeVersion": "8.0",
"digest": "9fcce259eb81c0b52ec7796b98a1653194e3a9021a1d338df1dbbab7522fc406"
},
- {
- "name": "point_right_tone2",
- "unicode": "1F449-1F3FC",
+ "point_right_tone2": {
+ "category": "people",
+ "moji": "👉🏼",
+ "unicodeVersion": "8.0",
"digest": "9d00a0b1cfc435674dc56065b3d28d28839196977504cf20581205351d8708f2"
},
- {
- "name": "point_right_tone3",
- "unicode": "1F449-1F3FD",
+ "point_right_tone3": {
+ "category": "people",
+ "moji": "👉🏽",
+ "unicodeVersion": "8.0",
"digest": "e3026a70630ba73d76892a055a80cac2f78d509faddce737f802d2abefa074ba"
},
- {
- "name": "point_right_tone4",
- "unicode": "1F449-1F3FE",
+ "point_right_tone4": {
+ "category": "people",
+ "moji": "👉🏾",
+ "unicodeVersion": "8.0",
"digest": "ea508fde90561460361773b4e1b8e80874667b19ac115926206e7c592587cb76"
},
- {
- "name": "point_right_tone5",
- "unicode": "1F449-1F3FF",
+ "point_right_tone5": {
+ "category": "people",
+ "moji": "👉🏿",
+ "unicodeVersion": "8.0",
"digest": "d59cdb2864eb2929941ecd233f8b8afcddc30fbd4594e5f9acf6386ae06ac12c"
},
- {
- "name": "point_up",
- "unicode": "261D",
+ "point_up": {
+ "category": "people",
+ "moji": "☝",
+ "unicodeVersion": "1.1",
"digest": "b69ff4f650989709f2185822d278c7773672bd9eb4a625da80f3038a2b9ce42b"
},
- {
- "name": "point_up_2",
- "unicode": "1F446",
+ "point_up_2": {
+ "category": "people",
+ "moji": "👆",
+ "unicodeVersion": "6.0",
"digest": "e83cd9eff2af5125a25f5a306c3ee3cfea240add683b5c36a86a994a8d8c805c"
},
- {
- "name": "point_up_2_tone1",
- "unicode": "1F446-1F3FB",
+ "point_up_2_tone1": {
+ "category": "people",
+ "moji": "👆🏻",
+ "unicodeVersion": "8.0",
"digest": "b02ec3e7e04a83bfb769cffb951cbf32aa78e56fa5a51c097f9326df9e08ed33"
},
- {
- "name": "point_up_2_tone2",
- "unicode": "1F446-1F3FC",
+ "point_up_2_tone2": {
+ "category": "people",
+ "moji": "👆🏼",
+ "unicodeVersion": "8.0",
"digest": "32994b85c8b4a1383ca985ebc3382be88866cea1ff1315adfb71fb05e992a232"
},
- {
- "name": "point_up_2_tone3",
- "unicode": "1F446-1F3FD",
+ "point_up_2_tone3": {
+ "category": "people",
+ "moji": "👆🏽",
+ "unicodeVersion": "8.0",
"digest": "9e263bcfb82ada34ff85291f36e64e66b86760fb11a4e0c554e801644d417d6d"
},
- {
- "name": "point_up_2_tone4",
- "unicode": "1F446-1F3FE",
+ "point_up_2_tone4": {
+ "category": "people",
+ "moji": "👆🏾",
+ "unicodeVersion": "8.0",
"digest": "3edc92130a0851ac7b5236772ce7918d088689221df287098688e1ed5b3ff181"
},
- {
- "name": "point_up_2_tone5",
- "unicode": "1F446-1F3FF",
+ "point_up_2_tone5": {
+ "category": "people",
+ "moji": "👆🏿",
+ "unicodeVersion": "8.0",
"digest": "cabb3b7da9290840ef59d0c8b22625bdb2e94842f01b0a575ccbc348f3069d77"
},
- {
- "name": "point_up_tone1",
- "unicode": "261D-1F3FB",
+ "point_up_tone1": {
+ "category": "people",
+ "moji": "☝🏻",
+ "unicodeVersion": "8.0",
"digest": "e496fda349072f8b321ceb7a251175f7244c3076661f5ede48ea75ba1acf8339"
},
- {
- "name": "point_up_tone2",
- "unicode": "261D-1F3FC",
+ "point_up_tone2": {
+ "category": "people",
+ "moji": "☝🏼",
+ "unicodeVersion": "8.0",
"digest": "5a8081323f3baa67e6431e21e16a36559b339f5175d586644e34947f738dd07a"
},
- {
- "name": "point_up_tone3",
- "unicode": "261D-1F3FD",
+ "point_up_tone3": {
+ "category": "people",
+ "moji": "☝🏽",
+ "unicodeVersion": "8.0",
"digest": "07bf0cea812eb226b443334e026e13d1ec23e013478f4af862a3919703107842"
},
- {
- "name": "point_up_tone4",
- "unicode": "261D-1F3FE",
+ "point_up_tone4": {
+ "category": "people",
+ "moji": "☝🏾",
+ "unicodeVersion": "8.0",
"digest": "1fbbd71433108143ee157d0fdadd183f7f013bafa96f0dd93b181e1fd5fd4af2"
},
- {
- "name": "point_up_tone5",
- "unicode": "261D-1F3FF",
+ "point_up_tone5": {
+ "category": "people",
+ "moji": "☝🏿",
+ "unicodeVersion": "8.0",
"digest": "ad068ef32df32f8297955490a9a90590a0f93ed5702a052cd0d8f6484c6cc679"
},
- {
- "name": "police_car",
- "unicode": "1F693",
+ "police_car": {
+ "category": "travel",
+ "moji": "🚓",
+ "unicodeVersion": "6.0",
"digest": "0909be1bd615ae331a7cce71e16dee3ca663c721d5170072c593cb7c76f9f661"
},
- {
- "name": "poodle",
- "unicode": "1F429",
+ "poodle": {
+ "category": "nature",
+ "moji": "🐩",
+ "unicodeVersion": "6.0",
"digest": "f1742fdf3fd26a8a5cfeaba57026518dacaad364cbd03344c4000a35af13e47a"
},
- {
- "name": "poop",
- "unicode": "1F4A9",
- "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
- },
- {
- "name": "shit",
- "unicode": "1F4A9",
+ "poop": {
+ "category": "people",
+ "moji": "💩",
+ "unicodeVersion": "6.0",
"digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
},
- {
- "name": "hankey",
- "unicode": "1F4A9",
- "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
- },
- {
- "name": "poo",
- "unicode": "1F4A9",
- "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec"
- },
- {
- "name": "popcorn",
- "unicode": "1F37F",
+ "popcorn": {
+ "category": "food",
+ "moji": "🍿",
+ "unicodeVersion": "8.0",
"digest": "684f1b7ef34ea7ca933aed41569bc6595a19ef0d546a1b7b9e69f8335540b323"
},
- {
- "name": "post_office",
- "unicode": "1F3E3",
+ "post_office": {
+ "category": "travel",
+ "moji": "🏣",
+ "unicodeVersion": "6.0",
"digest": "54398ee396c1314a7993b1cb1cba264946b5c9d5a7dbb43fd67286854d1d1a0f"
},
- {
- "name": "postal_horn",
- "unicode": "1F4EF",
+ "postal_horn": {
+ "category": "objects",
+ "moji": "📯",
+ "unicodeVersion": "6.0",
"digest": "0ea12f44f3bae9a14bde3b37361b48bd738d2f613bb1b53a9204959b70e643f8"
},
- {
- "name": "postbox",
- "unicode": "1F4EE",
+ "postbox": {
+ "category": "objects",
+ "moji": "📮",
+ "unicodeVersion": "6.0",
"digest": "bbc424ae8d46de380d7023a43ea064002fd614657d00330d3503275827ac87e2"
},
- {
- "name": "potable_water",
- "unicode": "1F6B0",
+ "potable_water": {
+ "category": "symbols",
+ "moji": "🚰",
+ "unicodeVersion": "6.0",
"digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098"
},
- {
- "name": "potato",
- "unicode": "1F954",
+ "potato": {
+ "category": "food",
+ "moji": "🥔",
+ "unicodeVersion": "9.0",
"digest": "a56a69f36f3a0793f278726d92c0cea2960554f3062ef1a0904526a04511d8e1"
},
- {
- "name": "pouch",
- "unicode": "1F45D",
+ "pouch": {
+ "category": "people",
+ "moji": "👝",
+ "unicodeVersion": "6.0",
"digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351"
},
- {
- "name": "poultry_leg",
- "unicode": "1F357",
+ "poultry_leg": {
+ "category": "food",
+ "moji": "🍗",
+ "unicodeVersion": "6.0",
"digest": "1445ec4f5e68a19e5a84e5537dca8190d62409070c954d112e6097f1a6b7f054"
},
- {
- "name": "pound",
- "unicode": "1F4B7",
+ "pound": {
+ "category": "objects",
+ "moji": "💷",
+ "unicodeVersion": "6.0",
"digest": "eb11b83eb52adb0a15e69a3bc15788a2dc7825dedee81ac3af84963c9dd517b5"
},
- {
- "name": "pouting_cat",
- "unicode": "1F63E",
+ "pouting_cat": {
+ "category": "people",
+ "moji": "😾",
+ "unicodeVersion": "6.0",
"digest": "8822abedf3499cf98278d7eeea0764d1100ec25cad71b4b2e877f9346f8c8138"
},
- {
- "name": "pray",
- "unicode": "1F64F",
+ "pray": {
+ "category": "people",
+ "moji": "🙏",
+ "unicodeVersion": "6.0",
"digest": "735b79dab34ac2cf81fd42fdcd7eb1f13c24655e5e343816d5764896c03edeea"
},
- {
- "name": "pray_tone1",
- "unicode": "1F64F-1F3FB",
+ "pray_tone1": {
+ "category": "people",
+ "moji": "🙏🏻",
+ "unicodeVersion": "8.0",
"digest": "e8b6103450215e8566797f150978355e297deade4eb47a6371f7a7bc558fed9d"
},
- {
- "name": "pray_tone2",
- "unicode": "1F64F-1F3FC",
+ "pray_tone2": {
+ "category": "people",
+ "moji": "🙏🏼",
+ "unicodeVersion": "8.0",
"digest": "ee8baacd95d7e8dbad8a1f2d9a12e36c98f3d518db5d3b117d0a18290815e62b"
},
- {
- "name": "pray_tone3",
- "unicode": "1F64F-1F3FD",
+ "pray_tone3": {
+ "category": "people",
+ "moji": "🙏🏽",
+ "unicodeVersion": "8.0",
"digest": "ae8c0caa9aca0a6c44069e76a7535c961d0284cd701812f76bbd2bd79ce2bd53"
},
- {
- "name": "pray_tone4",
- "unicode": "1F64F-1F3FE",
+ "pray_tone4": {
+ "category": "people",
+ "moji": "🙏🏾",
+ "unicodeVersion": "8.0",
"digest": "64f7b3178b8cd6f6a877ed583539eefe068fa87a0dd658fdcd58c8bc809f7e17"
},
- {
- "name": "pray_tone5",
- "unicode": "1F64F-1F3FF",
+ "pray_tone5": {
+ "category": "people",
+ "moji": "🙏🏿",
+ "unicodeVersion": "8.0",
"digest": "5bc8cdce937ac06779c87021423efcec4f602aa4a39dba90b00de81033005332"
},
- {
- "name": "prayer_beads",
- "unicode": "1F4FF",
+ "prayer_beads": {
+ "category": "objects",
+ "moji": "📿",
+ "unicodeVersion": "8.0",
"digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05"
},
- {
- "name": "pregnant_woman",
- "unicode": "1F930",
+ "pregnant_woman": {
+ "category": "people",
+ "moji": "🤰",
+ "unicodeVersion": "9.0",
"digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
},
- {
- "name": "expecting_woman",
- "unicode": "1F930",
- "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
- },
- {
- "name": "pregnant_woman_tone1",
- "unicode": "1F930-1F3FB",
- "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
- },
- {
- "name": "expecting_woman_tone1",
- "unicode": "1F930-1F3FB",
+ "pregnant_woman_tone1": {
+ "category": "people",
+ "moji": "🤰🏻",
+ "unicodeVersion": "9.0",
"digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
},
- {
- "name": "pregnant_woman_tone2",
- "unicode": "1F930-1F3FC",
+ "pregnant_woman_tone2": {
+ "category": "people",
+ "moji": "🤰🏼",
+ "unicodeVersion": "9.0",
"digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
},
- {
- "name": "expecting_woman_tone2",
- "unicode": "1F930-1F3FC",
- "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
- },
- {
- "name": "pregnant_woman_tone3",
- "unicode": "1F930-1F3FD",
+ "pregnant_woman_tone3": {
+ "category": "people",
+ "moji": "🤰🏽",
+ "unicodeVersion": "9.0",
"digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
},
- {
- "name": "expecting_woman_tone3",
- "unicode": "1F930-1F3FD",
- "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
- },
- {
- "name": "pregnant_woman_tone4",
- "unicode": "1F930-1F3FE",
+ "pregnant_woman_tone4": {
+ "category": "people",
+ "moji": "🤰🏾",
+ "unicodeVersion": "9.0",
"digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
},
- {
- "name": "expecting_woman_tone4",
- "unicode": "1F930-1F3FE",
- "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
- },
- {
- "name": "pregnant_woman_tone5",
- "unicode": "1F930-1F3FF",
- "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
- },
- {
- "name": "expecting_woman_tone5",
- "unicode": "1F930-1F3FF",
+ "pregnant_woman_tone5": {
+ "category": "people",
+ "moji": "🤰🏿",
+ "unicodeVersion": "9.0",
"digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
},
- {
- "name": "prince",
- "unicode": "1F934",
+ "prince": {
+ "category": "people",
+ "moji": "🤴",
+ "unicodeVersion": "9.0",
"digest": "34a0e0625f0a9825d3674192d6233b6cae4d8130451293df09f91a6a4165869c"
},
- {
- "name": "prince_tone1",
- "unicode": "1F934-1F3FB",
+ "prince_tone1": {
+ "category": "people",
+ "moji": "🤴🏻",
+ "unicodeVersion": "9.0",
"digest": "ccecdfeccb2ab1fceceae14f3fba875c8c7099785a4c40131c08a697b5b675fc"
},
- {
- "name": "prince_tone2",
- "unicode": "1F934-1F3FC",
+ "prince_tone2": {
+ "category": "people",
+ "moji": "🤴🏼",
+ "unicodeVersion": "9.0",
"digest": "c373fd3e0c1798415e3d8d88fab6c98c1bbdedcbe6f52f3a3899f6e2124a768d"
},
- {
- "name": "prince_tone3",
- "unicode": "1F934-1F3FD",
+ "prince_tone3": {
+ "category": "people",
+ "moji": "🤴🏽",
+ "unicodeVersion": "9.0",
"digest": "71d15695ca954d55aa69d3c753c7d31a8ba5329713a8ddbc90dafc11e524c4ef"
},
- {
- "name": "prince_tone4",
- "unicode": "1F934-1F3FE",
+ "prince_tone4": {
+ "category": "people",
+ "moji": "🤴🏾",
+ "unicodeVersion": "9.0",
"digest": "08f6cb32424f15cc3aaf83c31a5dac7c01a6be2f37ea8f13aed579ce6fb4db19"
},
- {
- "name": "prince_tone5",
- "unicode": "1F934-1F3FF",
+ "prince_tone5": {
+ "category": "people",
+ "moji": "🤴🏿",
+ "unicodeVersion": "9.0",
"digest": "77d521148efa33fa4d3409693d050fecfd948411e807327484f174e289834649"
},
- {
- "name": "princess",
- "unicode": "1F478",
+ "princess": {
+ "category": "people",
+ "moji": "👸",
+ "unicodeVersion": "6.0",
"digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80"
},
- {
- "name": "princess_tone1",
- "unicode": "1F478-1F3FB",
+ "princess_tone1": {
+ "category": "people",
+ "moji": "👸🏻",
+ "unicodeVersion": "8.0",
"digest": "52b88b99ba64f82e8f36e2a1827c85145e4fcd6863478c2345fe9fa9e8901cdf"
},
- {
- "name": "princess_tone2",
- "unicode": "1F478-1F3FC",
+ "princess_tone2": {
+ "category": "people",
+ "moji": "👸🏼",
+ "unicodeVersion": "8.0",
"digest": "7e44289404693668f20e681fcdc2e516512d54a69c627eedae958f69dfe6eea9"
},
- {
- "name": "princess_tone3",
- "unicode": "1F478-1F3FD",
+ "princess_tone3": {
+ "category": "people",
+ "moji": "👸🏽",
+ "unicodeVersion": "8.0",
"digest": "96c9a9857348d7a1a8be899c50d55b352b9a9fd5c65e4777bfa199fe7929d41c"
},
- {
- "name": "princess_tone4",
- "unicode": "1F478-1F3FE",
+ "princess_tone4": {
+ "category": "people",
+ "moji": "👸🏾",
+ "unicodeVersion": "8.0",
"digest": "67696f96be60f2a36598072172d2db197d007e6c1ac3acef526a5ce6d59bf3f7"
},
- {
- "name": "princess_tone5",
- "unicode": "1F478-1F3FF",
+ "princess_tone5": {
+ "category": "people",
+ "moji": "👸🏿",
+ "unicodeVersion": "8.0",
"digest": "007f624e2fad91bb57ce32ecd35213a796d71807f3b12f3f1575bf50e6a50eeb"
},
- {
- "name": "printer",
- "unicode": "1F5A8",
+ "printer": {
+ "category": "objects",
+ "moji": "🖨",
+ "unicodeVersion": "7.0",
"digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8"
},
- {
- "name": "projector",
- "unicode": "1F4FD",
+ "projector": {
+ "category": "objects",
+ "moji": "📽",
+ "unicodeVersion": "7.0",
"digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
},
- {
- "name": "film_projector",
- "unicode": "1F4FD",
- "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
- },
- {
- "name": "punch",
- "unicode": "1F44A",
+ "punch": {
+ "category": "people",
+ "moji": "👊",
+ "unicodeVersion": "6.0",
"digest": "c7e7edf6d64f755db3f02874354f08337b3971aff329476d19ac946e0b421329"
},
- {
- "name": "punch_tone1",
- "unicode": "1F44A-1F3FB",
+ "punch_tone1": {
+ "category": "people",
+ "moji": "👊🏻",
+ "unicodeVersion": "8.0",
"digest": "c9ba508b0c36041047473782acfedab5af40dd7946b33daf4d8d54c726e06a11"
},
- {
- "name": "punch_tone2",
- "unicode": "1F44A-1F3FC",
+ "punch_tone2": {
+ "category": "people",
+ "moji": "👊🏼",
+ "unicodeVersion": "8.0",
"digest": "d53011cd2f3334c7b3fffdfe1e2b8cc1c832c74306e1ac6d03f954a1309d7d0b"
},
- {
- "name": "punch_tone3",
- "unicode": "1F44A-1F3FD",
+ "punch_tone3": {
+ "category": "people",
+ "moji": "👊🏽",
+ "unicodeVersion": "8.0",
"digest": "f7522347094e0130ed8e304678106574dbd7dd2b6b3aeb4d8a7a0fef880920b2"
},
- {
- "name": "punch_tone4",
- "unicode": "1F44A-1F3FE",
+ "punch_tone4": {
+ "category": "people",
+ "moji": "👊🏾",
+ "unicodeVersion": "8.0",
"digest": "3e62bdd426f3e6ff175ce3b8dd6f6d3998d9c1506128defa96b528b455295b47"
},
- {
- "name": "punch_tone5",
- "unicode": "1F44A-1F3FF",
+ "punch_tone5": {
+ "category": "people",
+ "moji": "👊🏿",
+ "unicodeVersion": "8.0",
"digest": "7d9bff777dc4ec41ac132b1252fa08cf92a398c8dc146c4a5327b45d568982d8"
},
- {
- "name": "purple_heart",
- "unicode": "1F49C",
+ "purple_heart": {
+ "category": "symbols",
+ "moji": "💜",
+ "unicodeVersion": "6.0",
"digest": "a6bf01de806525942be480e45a4b2879f91df8129b78a1b8734d4f917bcab773"
},
- {
- "name": "purse",
- "unicode": "1F45B",
+ "purse": {
+ "category": "people",
+ "moji": "👛",
+ "unicodeVersion": "6.0",
"digest": "2b785f36e01875d66cfda2192c8c53606e7224a7c869a4826b62cb61613d60c8"
},
- {
- "name": "pushpin",
- "unicode": "1F4CC",
+ "pushpin": {
+ "category": "objects",
+ "moji": "📌",
+ "unicodeVersion": "6.0",
"digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8"
},
- {
- "name": "put_litter_in_its_place",
- "unicode": "1F6AE",
+ "put_litter_in_its_place": {
+ "category": "symbols",
+ "moji": "🚮",
+ "unicodeVersion": "6.0",
"digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c"
},
- {
- "name": "question",
- "unicode": "2753",
+ "question": {
+ "category": "symbols",
+ "moji": "❓",
+ "unicodeVersion": "6.0",
"digest": "40050a1fd29bed321fd601d13dc33de5d6084121f1d873b29bde9dc3d823a310"
},
- {
- "name": "rabbit",
- "unicode": "1F430",
+ "rabbit": {
+ "category": "nature",
+ "moji": "🐰",
+ "unicodeVersion": "6.0",
"digest": "678ad953a7ab8f618c59051449a67c965d1f04f42dd6f6669adaf3fadebd080c"
},
- {
- "name": "rabbit2",
- "unicode": "1F407",
+ "rabbit2": {
+ "category": "nature",
+ "moji": "🐇",
+ "unicodeVersion": "6.0",
"digest": "19b1f5108292472434cc7a49efac4ea9275779735c7aeb0f15c36021d5998ca0"
},
- {
- "name": "race_car",
- "unicode": "1F3CE",
- "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
- },
- {
- "name": "racing_car",
- "unicode": "1F3CE",
+ "race_car": {
+ "category": "travel",
+ "moji": "🏎",
+ "unicodeVersion": "7.0",
"digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6"
},
- {
- "name": "racehorse",
- "unicode": "1F40E",
+ "racehorse": {
+ "category": "nature",
+ "moji": "🐎",
+ "unicodeVersion": "6.0",
"digest": "a57b7aca35347ada8225eeee06b70cfd040484104963b4df56ea8fec690576b0"
},
- {
- "name": "radio",
- "unicode": "1F4FB",
+ "radio": {
+ "category": "objects",
+ "moji": "📻",
+ "unicodeVersion": "6.0",
"digest": "9245951dd779cdd141089891b15a90d3999a6358acf1fc296aa505100f812108"
},
- {
- "name": "radio_button",
- "unicode": "1F518",
+ "radio_button": {
+ "category": "symbols",
+ "moji": "🔘",
+ "unicodeVersion": "6.0",
"digest": "565bec59198df2592e96564c6e314d3cde33c47b453db1bec6c5d027b5cb4fd9"
},
- {
- "name": "radioactive",
- "unicode": "2622",
- "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
- },
- {
- "name": "radioactive_sign",
- "unicode": "2622",
+ "radioactive": {
+ "category": "symbols",
+ "moji": "☢",
+ "unicodeVersion": "1.1",
"digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581"
},
- {
- "name": "rage",
- "unicode": "1F621",
+ "rage": {
+ "category": "people",
+ "moji": "😡",
+ "unicodeVersion": "6.0",
"digest": "d97ba6bd08eec46dbc7199f530c945b73a87a878e35397b0a3e4f2b45039e89e"
},
- {
- "name": "railway_car",
- "unicode": "1F683",
+ "railway_car": {
+ "category": "travel",
+ "moji": "🚃",
+ "unicodeVersion": "6.0",
"digest": "2cddc08d555e7fc24e312c3d255ed013fbf9cd2974a6918369c32554049ba2be"
},
- {
- "name": "railway_track",
- "unicode": "1F6E4",
- "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
- },
- {
- "name": "railroad_track",
- "unicode": "1F6E4",
+ "railway_track": {
+ "category": "travel",
+ "moji": "🛤",
+ "unicodeVersion": "7.0",
"digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7"
},
- {
- "name": "rainbow",
- "unicode": "1F308",
+ "rainbow": {
+ "category": "travel",
+ "moji": "🌈",
+ "unicodeVersion": "6.0",
"digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d"
},
- {
- "name": "raised_back_of_hand",
- "unicode": "1F91A",
+ "raised_back_of_hand": {
+ "category": "people",
+ "moji": "🤚",
+ "unicodeVersion": "9.0",
"digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
},
- {
- "name": "back_of_hand",
- "unicode": "1F91A",
- "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
- },
- {
- "name": "raised_back_of_hand_tone1",
- "unicode": "1F91A-1F3FB",
- "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
- },
- {
- "name": "back_of_hand_tone1",
- "unicode": "1F91A-1F3FB",
+ "raised_back_of_hand_tone1": {
+ "category": "people",
+ "moji": "🤚🏻",
+ "unicodeVersion": "9.0",
"digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
},
- {
- "name": "raised_back_of_hand_tone2",
- "unicode": "1F91A-1F3FC",
- "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
- },
- {
- "name": "back_of_hand_tone2",
- "unicode": "1F91A-1F3FC",
+ "raised_back_of_hand_tone2": {
+ "category": "people",
+ "moji": "🤚🏼",
+ "unicodeVersion": "9.0",
"digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
},
- {
- "name": "raised_back_of_hand_tone3",
- "unicode": "1F91A-1F3FD",
- "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
- },
- {
- "name": "back_of_hand_tone3",
- "unicode": "1F91A-1F3FD",
+ "raised_back_of_hand_tone3": {
+ "category": "people",
+ "moji": "🤚🏽",
+ "unicodeVersion": "9.0",
"digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
},
- {
- "name": "raised_back_of_hand_tone4",
- "unicode": "1F91A-1F3FE",
- "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
- },
- {
- "name": "back_of_hand_tone4",
- "unicode": "1F91A-1F3FE",
+ "raised_back_of_hand_tone4": {
+ "category": "people",
+ "moji": "🤚🏾",
+ "unicodeVersion": "9.0",
"digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
},
- {
- "name": "raised_back_of_hand_tone5",
- "unicode": "1F91A-1F3FF",
- "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
- },
- {
- "name": "back_of_hand_tone5",
- "unicode": "1F91A-1F3FF",
+ "raised_back_of_hand_tone5": {
+ "category": "people",
+ "moji": "🤚🏿",
+ "unicodeVersion": "9.0",
"digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
},
- {
- "name": "raised_hand",
- "unicode": "270B",
+ "raised_hand": {
+ "category": "people",
+ "moji": "✋",
+ "unicodeVersion": "6.0",
"digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a"
},
- {
- "name": "raised_hand_tone1",
- "unicode": "270B-1F3FB",
+ "raised_hand_tone1": {
+ "category": "people",
+ "moji": "✋🏻",
+ "unicodeVersion": "8.0",
"digest": "865afca29b57577fed8fe8c2be57b74254a008c8cf34194680be2759239b5f5d"
},
- {
- "name": "raised_hand_tone2",
- "unicode": "270B-1F3FC",
+ "raised_hand_tone2": {
+ "category": "people",
+ "moji": "✋🏼",
+ "unicodeVersion": "8.0",
"digest": "832169a0b626a682a58a3b998f68413657b4962c1fab05f1fdc2668e82727210"
},
- {
- "name": "raised_hand_tone3",
- "unicode": "270B-1F3FD",
+ "raised_hand_tone3": {
+ "category": "people",
+ "moji": "✋🏽",
+ "unicodeVersion": "8.0",
"digest": "3959a873ad7671de82c615c4ed840b011e67baafb2bab7dd16859608d3e83cb1"
},
- {
- "name": "raised_hand_tone4",
- "unicode": "270B-1F3FE",
+ "raised_hand_tone4": {
+ "category": "people",
+ "moji": "✋🏾",
+ "unicodeVersion": "8.0",
"digest": "db542f65d076ccf3dbfca27cb7c2f135a8bf7a487a81a04873e70172bdfcd579"
},
- {
- "name": "raised_hand_tone5",
- "unicode": "270B-1F3FF",
+ "raised_hand_tone5": {
+ "category": "people",
+ "moji": "✋🏿",
+ "unicodeVersion": "8.0",
"digest": "88ca884d14baaae48df21d75c22d82fb15bdc395e42026f5ca34cd65e5ae8674"
},
- {
- "name": "raised_hands",
- "unicode": "1F64C",
+ "raised_hands": {
+ "category": "people",
+ "moji": "🙌",
+ "unicodeVersion": "6.0",
"digest": "2ee73466a3f5079e542857fe6f5497e9f87753a81854985ce3356a8d3da1d8b8"
},
- {
- "name": "raised_hands_tone1",
- "unicode": "1F64C-1F3FB",
+ "raised_hands_tone1": {
+ "category": "people",
+ "moji": "🙌🏻",
+ "unicodeVersion": "8.0",
"digest": "43e73c60f040a66374b8ec98f3629a90d13ae9f472446ed7676cd5573e824f4b"
},
- {
- "name": "raised_hands_tone2",
- "unicode": "1F64C-1F3FC",
+ "raised_hands_tone2": {
+ "category": "people",
+ "moji": "🙌🏼",
+ "unicodeVersion": "8.0",
"digest": "fcc5255bb2b06dc82d6878e74cf34e8ce118c70004a06d39a980683772b98c52"
},
- {
- "name": "raised_hands_tone3",
- "unicode": "1F64C-1F3FD",
+ "raised_hands_tone3": {
+ "category": "people",
+ "moji": "🙌🏽",
+ "unicodeVersion": "8.0",
"digest": "3ee3e0aafef486e766a166935e8147fb75a7329cfebc96dec876cc45e83a8754"
},
- {
- "name": "raised_hands_tone4",
- "unicode": "1F64C-1F3FE",
+ "raised_hands_tone4": {
+ "category": "people",
+ "moji": "🙌🏾",
+ "unicodeVersion": "8.0",
"digest": "78a8cbf6b2b85be4d6b18f0ff6a77f197963117955725fb7e57e0441effb928f"
},
- {
- "name": "raised_hands_tone5",
- "unicode": "1F64C-1F3FF",
+ "raised_hands_tone5": {
+ "category": "people",
+ "moji": "🙌🏿",
+ "unicodeVersion": "8.0",
"digest": "2a5ed7334a17172db0cd820a559e7f75df40ec44de6c25d194c76e1b58c634cb"
},
- {
- "name": "raising_hand",
- "unicode": "1F64B",
+ "raising_hand": {
+ "category": "people",
+ "moji": "🙋",
+ "unicodeVersion": "6.0",
"digest": "512750b00704f1ccefd3c757743540b785ad7670dbbe4a2c4dca8d93e6701920"
},
- {
- "name": "raising_hand_tone1",
- "unicode": "1F64B-1F3FB",
+ "raising_hand_tone1": {
+ "category": "people",
+ "moji": "🙋🏻",
+ "unicodeVersion": "8.0",
"digest": "2897722f091c273dd3714cff7423c2475bc3070416c28014ca03322b9ece48bc"
},
- {
- "name": "raising_hand_tone2",
- "unicode": "1F64B-1F3FC",
+ "raising_hand_tone2": {
+ "category": "people",
+ "moji": "🙋🏼",
+ "unicodeVersion": "8.0",
"digest": "59199b334b3845911382c1f29bd7c0d5ef9d2486417345e265b166ead7d3e1c1"
},
- {
- "name": "raising_hand_tone3",
- "unicode": "1F64B-1F3FD",
+ "raising_hand_tone3": {
+ "category": "people",
+ "moji": "🙋🏽",
+ "unicodeVersion": "8.0",
"digest": "f95b338d5efcf14ef12f415a2c1bba93df48628ddc94f34f70c31e1b3c2e1d28"
},
- {
- "name": "raising_hand_tone4",
- "unicode": "1F64B-1F3FE",
+ "raising_hand_tone4": {
+ "category": "people",
+ "moji": "🙋🏾",
+ "unicodeVersion": "8.0",
"digest": "951ddbfdb57d5a60551b59b3d0f7ca00a64912f4a101a73afaebd68445cd6cec"
},
- {
- "name": "raising_hand_tone5",
- "unicode": "1F64B-1F3FF",
+ "raising_hand_tone5": {
+ "category": "people",
+ "moji": "🙋🏿",
+ "unicodeVersion": "8.0",
"digest": "9370f93704d8f89ca6dc946715eab5e7dba82bf04dd68c00f5c0abb8bc16371e"
},
- {
- "name": "ram",
- "unicode": "1F40F",
+ "ram": {
+ "category": "nature",
+ "moji": "🐏",
+ "unicodeVersion": "6.0",
"digest": "2875ab28e1018b39062aeb0c5ce488c48a98f13e9f2364470a0a700b126604f2"
},
- {
- "name": "ramen",
- "unicode": "1F35C",
+ "ramen": {
+ "category": "food",
+ "moji": "🍜",
+ "unicodeVersion": "6.0",
"digest": "425662a49c4c13577c0de8d45d004e5ba204aaadbaabae62a5c283ecd7a9a2c5"
},
- {
- "name": "rat",
- "unicode": "1F400",
+ "rat": {
+ "category": "nature",
+ "moji": "🐀",
+ "unicodeVersion": "6.0",
"digest": "14380d65498c6ce037c02a93bca2b24f25a368d85278d6015b8c9f7cd261f8e2"
},
- {
- "name": "record_button",
- "unicode": "23FA",
+ "record_button": {
+ "category": "symbols",
+ "moji": "⏺",
+ "unicodeVersion": "7.0",
"digest": "92be12161ba206bb2e06a39131711c7b17368d55b4aae0b48f0ac5b6b1cde76b"
},
- {
- "name": "recycle",
- "unicode": "267B",
+ "recycle": {
+ "category": "symbols",
+ "moji": "♻",
+ "unicodeVersion": "3.2",
"digest": "c377e8537367b05b5de9be860a0fcabd7aed2bf4ba146eefc423671a21530369"
},
- {
- "name": "red_car",
- "unicode": "1F697",
+ "red_car": {
+ "category": "travel",
+ "moji": "🚗",
+ "unicodeVersion": "6.0",
"digest": "8a99832a195263c0e922af53d52dea37aa3e07032b3c2a1977f8527b4a144b9c"
},
- {
- "name": "red_circle",
- "unicode": "1F534",
+ "red_circle": {
+ "category": "symbols",
+ "moji": "🔴",
+ "unicodeVersion": "6.0",
"digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307"
},
- {
- "name": "registered",
- "unicode": "00AE",
+ "registered": {
+ "category": "symbols",
+ "moji": "®",
+ "unicodeVersion": "1.1",
"digest": "9661b1df529ecb752d130820c55c403e5de263748eb02f7fea327818bc282d94"
},
- {
- "name": "relaxed",
- "unicode": "263A",
+ "relaxed": {
+ "category": "people",
+ "moji": "☺",
+ "unicodeVersion": "1.1",
"digest": "2d5aed4fb8504c6d6660ef8d3bfe0cc053dcd6099c2f53748c202dc970c639bc"
},
- {
- "name": "relieved",
- "unicode": "1F60C",
+ "relieved": {
+ "category": "people",
+ "moji": "😌",
+ "unicodeVersion": "6.0",
"digest": "b4ce2ba6c220d887fe5e333c05ed773df9b6df0ac456879fd8f5103ff68604a5"
},
- {
- "name": "reminder_ribbon",
- "unicode": "1F397",
+ "reminder_ribbon": {
+ "category": "activity",
+ "moji": "🎗",
+ "unicodeVersion": "7.0",
"digest": "c3de2a7c9350b77a0b86c0dcce9dcd9953ea8a97aa1e7aed149755924742f54d"
},
- {
- "name": "repeat",
- "unicode": "1F501",
+ "repeat": {
+ "category": "symbols",
+ "moji": "🔁",
+ "unicodeVersion": "6.0",
"digest": "b9512d508613ed0eb3181eb1030f7f6fd6b994476ecdfa308733c6df975fb99e"
},
- {
- "name": "repeat_one",
- "unicode": "1F502",
+ "repeat_one": {
+ "category": "symbols",
+ "moji": "🔂",
+ "unicodeVersion": "6.0",
"digest": "53409cf24dd4bb0d7b50ae359f15d06b87b7f4a292ed5c3a09652fa421a90bf2"
},
- {
- "name": "restroom",
- "unicode": "1F6BB",
+ "restroom": {
+ "category": "symbols",
+ "moji": "🚻",
+ "unicodeVersion": "6.0",
"digest": "2e7a1bfc9a9d49b0272230a91db7369e24d54bf1de8e683d36b85f1d8c037f77"
},
- {
- "name": "revolving_hearts",
- "unicode": "1F49E",
+ "revolving_hearts": {
+ "category": "symbols",
+ "moji": "💞",
+ "unicodeVersion": "6.0",
"digest": "c43d3197cb4cf06659f643638f6c4e91a2889e0f6531b7d81ea826c2a8b784fc"
},
- {
- "name": "rewind",
- "unicode": "23EA",
+ "rewind": {
+ "category": "symbols",
+ "moji": "⏪",
+ "unicodeVersion": "6.0",
"digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd"
},
- {
- "name": "rhino",
- "unicode": "1F98F",
- "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
- },
- {
- "name": "rhinoceros",
- "unicode": "1F98F",
+ "rhino": {
+ "category": "nature",
+ "moji": "🦏",
+ "unicodeVersion": "9.0",
"digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
},
- {
- "name": "ribbon",
- "unicode": "1F380",
+ "ribbon": {
+ "category": "objects",
+ "moji": "🎀",
+ "unicodeVersion": "6.0",
"digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828"
},
- {
- "name": "rice",
- "unicode": "1F35A",
+ "rice": {
+ "category": "food",
+ "moji": "🍚",
+ "unicodeVersion": "6.0",
"digest": "f544f12606de59d28739798003f14ebd8869856add8e24496ec5dda3e131daf4"
},
- {
- "name": "rice_ball",
- "unicode": "1F359",
+ "rice_ball": {
+ "category": "food",
+ "moji": "🍙",
+ "unicodeVersion": "6.0",
"digest": "2cba6f5364cd366859bc8948897b65fc97b225ea7973d9be3b24aba388fed8e8"
},
- {
- "name": "rice_cracker",
- "unicode": "1F358",
+ "rice_cracker": {
+ "category": "food",
+ "moji": "🍘",
+ "unicodeVersion": "6.0",
"digest": "ac0f805d41d4f322154c1968bd3ce3e9aabcd39d908182e52fd7d28458dbef92"
},
- {
- "name": "rice_scene",
- "unicode": "1F391",
+ "rice_scene": {
+ "category": "travel",
+ "moji": "🎑",
+ "unicodeVersion": "6.0",
"digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5"
},
- {
- "name": "right_facing_fist",
- "unicode": "1F91C",
- "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
- },
- {
- "name": "right_fist",
- "unicode": "1F91C",
+ "right_facing_fist": {
+ "category": "people",
+ "moji": "🤜",
+ "unicodeVersion": "9.0",
"digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
},
- {
- "name": "right_facing_fist_tone1",
- "unicode": "1F91C-1F3FB",
- "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
- },
- {
- "name": "right_fist_tone1",
- "unicode": "1F91C-1F3FB",
+ "right_facing_fist_tone1": {
+ "category": "people",
+ "moji": "🤜🏻",
+ "unicodeVersion": "9.0",
"digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
},
- {
- "name": "right_facing_fist_tone2",
- "unicode": "1F91C-1F3FC",
- "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
- },
- {
- "name": "right_fist_tone2",
- "unicode": "1F91C-1F3FC",
+ "right_facing_fist_tone2": {
+ "category": "people",
+ "moji": "🤜🏼",
+ "unicodeVersion": "9.0",
"digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
},
- {
- "name": "right_facing_fist_tone3",
- "unicode": "1F91C-1F3FD",
- "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
- },
- {
- "name": "right_fist_tone3",
- "unicode": "1F91C-1F3FD",
+ "right_facing_fist_tone3": {
+ "category": "people",
+ "moji": "🤜🏽",
+ "unicodeVersion": "9.0",
"digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
},
- {
- "name": "right_facing_fist_tone4",
- "unicode": "1F91C-1F3FE",
+ "right_facing_fist_tone4": {
+ "category": "people",
+ "moji": "🤜🏾",
+ "unicodeVersion": "9.0",
"digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
},
- {
- "name": "right_fist_tone4",
- "unicode": "1F91C-1F3FE",
- "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
- },
- {
- "name": "right_facing_fist_tone5",
- "unicode": "1F91C-1F3FF",
- "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
- },
- {
- "name": "right_fist_tone5",
- "unicode": "1F91C-1F3FF",
+ "right_facing_fist_tone5": {
+ "category": "people",
+ "moji": "🤜🏿",
+ "unicodeVersion": "9.0",
"digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
},
- {
- "name": "ring",
- "unicode": "1F48D",
+ "ring": {
+ "category": "people",
+ "moji": "💍",
+ "unicodeVersion": "6.0",
"digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d"
},
- {
- "name": "robot",
- "unicode": "1F916",
+ "robot": {
+ "category": "people",
+ "moji": "🤖",
+ "unicodeVersion": "8.0",
"digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
},
- {
- "name": "robot_face",
- "unicode": "1F916",
- "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
- },
- {
- "name": "rocket",
- "unicode": "1F680",
+ "rocket": {
+ "category": "travel",
+ "moji": "🚀",
+ "unicodeVersion": "6.0",
"digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d"
},
- {
- "name": "rofl",
- "unicode": "1F923",
- "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
- },
- {
- "name": "rolling_on_the_floor_laughing",
- "unicode": "1F923",
+ "rofl": {
+ "category": "people",
+ "moji": "🤣",
+ "unicodeVersion": "9.0",
"digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
},
- {
- "name": "roller_coaster",
- "unicode": "1F3A2",
+ "roller_coaster": {
+ "category": "travel",
+ "moji": "🎢",
+ "unicodeVersion": "6.0",
"digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c"
},
- {
- "name": "rolling_eyes",
- "unicode": "1F644",
+ "rolling_eyes": {
+ "category": "people",
+ "moji": "🙄",
+ "unicodeVersion": "8.0",
"digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
},
- {
- "name": "face_with_rolling_eyes",
- "unicode": "1F644",
- "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7"
- },
- {
- "name": "rooster",
- "unicode": "1F413",
+ "rooster": {
+ "category": "nature",
+ "moji": "🐓",
+ "unicodeVersion": "6.0",
"digest": "2b90c5cf6fa46da13eb77285443d600afcea0c48bd1d215d60167e7dc510da5d"
},
- {
- "name": "rose",
- "unicode": "1F339",
+ "rose": {
+ "category": "nature",
+ "moji": "🌹",
+ "unicodeVersion": "6.0",
"digest": "73799e459dba188de4de704605d824242feeb65d587c5bf9109acf528d037146"
},
- {
- "name": "rosette",
- "unicode": "1F3F5",
+ "rosette": {
+ "category": "activity",
+ "moji": "🏵",
+ "unicodeVersion": "7.0",
"digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5"
},
- {
- "name": "rotating_light",
- "unicode": "1F6A8",
+ "rotating_light": {
+ "category": "travel",
+ "moji": "🚨",
+ "unicodeVersion": "6.0",
"digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f"
},
- {
- "name": "round_pushpin",
- "unicode": "1F4CD",
+ "round_pushpin": {
+ "category": "objects",
+ "moji": "📍",
+ "unicodeVersion": "6.0",
"digest": "8ffca77bbdc6f1f726daf3abd6eff338a5ad1aa9b09dbbd8782c1e7ef5452f30"
},
- {
- "name": "rowboat",
- "unicode": "1F6A3",
+ "rowboat": {
+ "category": "activity",
+ "moji": "🚣",
+ "unicodeVersion": "6.0",
"digest": "83715d83a061926d4ad3bb569b21f5d337e3ebd4c9bcdfe493e661c12adc0a16"
},
- {
- "name": "rowboat_tone1",
- "unicode": "1F6A3-1F3FB",
+ "rowboat_tone1": {
+ "category": "activity",
+ "moji": "🚣🏻",
+ "unicodeVersion": "8.0",
"digest": "e279ac816442c0876fba1f42c700b80f2fb6de671e1a8a9e9d11b71eed5c58e8"
},
- {
- "name": "rowboat_tone2",
- "unicode": "1F6A3-1F3FC",
+ "rowboat_tone2": {
+ "category": "activity",
+ "moji": "🚣🏼",
+ "unicodeVersion": "8.0",
"digest": "6a48eba352ed4971d26498b6c622e5772389c89c5205ed02acde8e995dddcc3b"
},
- {
- "name": "rowboat_tone3",
- "unicode": "1F6A3-1F3FD",
+ "rowboat_tone3": {
+ "category": "activity",
+ "moji": "🚣🏽",
+ "unicodeVersion": "8.0",
"digest": "875948f6d8354ebd95ce9a66fde30f06a8366dcd89d5ca3e660845f8801e9305"
},
- {
- "name": "rowboat_tone4",
- "unicode": "1F6A3-1F3FE",
+ "rowboat_tone4": {
+ "category": "activity",
+ "moji": "🚣🏾",
+ "unicodeVersion": "8.0",
"digest": "8c7ac7346b0020d0ff5e2f4a1efb1b7785eac637f17556663ec33e2335083f0a"
},
- {
- "name": "rowboat_tone5",
- "unicode": "1F6A3-1F3FF",
+ "rowboat_tone5": {
+ "category": "activity",
+ "moji": "🚣🏿",
+ "unicodeVersion": "8.0",
"digest": "a399dbb647892b22323e0bf17bc36a9b5f1708ebedf9ba525233ee7b9d48339a"
},
- {
- "name": "rugby_football",
- "unicode": "1F3C9",
+ "rugby_football": {
+ "category": "activity",
+ "moji": "🏉",
+ "unicodeVersion": "6.0",
"digest": "cc6f00ade3e0bbb7899e7bfb138b57216dd66de26d7967d5ffa501f382ed09f4"
},
- {
- "name": "runner",
- "unicode": "1F3C3",
+ "runner": {
+ "category": "people",
+ "moji": "🏃",
+ "unicodeVersion": "6.0",
"digest": "e9af7b591be60ade2049dbada0f062ba2d3e17f02bec76cbd34ce68854a2a10c"
},
- {
- "name": "runner_tone1",
- "unicode": "1F3C3-1F3FB",
+ "runner_tone1": {
+ "category": "people",
+ "moji": "🏃🏻",
+ "unicodeVersion": "8.0",
"digest": "21091cbb09c558712ecf63548bf28b7995df42bdb85235088799a517800e52f5"
},
- {
- "name": "runner_tone2",
- "unicode": "1F3C3-1F3FC",
+ "runner_tone2": {
+ "category": "people",
+ "moji": "🏃🏼",
+ "unicodeVersion": "8.0",
"digest": "1fe3d194f675a46fe67799394192e66c407dd81163363692c5e7da32ddb9af2b"
},
- {
- "name": "runner_tone3",
- "unicode": "1F3C3-1F3FD",
+ "runner_tone3": {
+ "category": "people",
+ "moji": "🏃🏽",
+ "unicodeVersion": "8.0",
"digest": "8cea1bf4ef3be71f42dc5bae978d5b7a197a3851543225349ef0dda29a370537"
},
- {
- "name": "runner_tone4",
- "unicode": "1F3C3-1F3FE",
+ "runner_tone4": {
+ "category": "people",
+ "moji": "🏃🏾",
+ "unicodeVersion": "8.0",
"digest": "c33f0b8b5a71d295fb6ba322e79446964a8eca9e4573efd591e4273808b088a0"
},
- {
- "name": "runner_tone5",
- "unicode": "1F3C3-1F3FF",
+ "runner_tone5": {
+ "category": "people",
+ "moji": "🏃🏿",
+ "unicodeVersion": "8.0",
"digest": "9f59e6dd0fdf2f17bceb41f5c355b4e6f3c8bb8cbd8af0992f0b5630ff8892e8"
},
- {
- "name": "running_shirt_with_sash",
- "unicode": "1F3BD",
+ "running_shirt_with_sash": {
+ "category": "activity",
+ "moji": "🎽",
+ "unicodeVersion": "6.0",
"digest": "7542307d3595aca45e8ccae66b6e58b6e92870144b738263d5379ec6dc992b76"
},
- {
- "name": "sa",
- "unicode": "1F202",
+ "sa": {
+ "category": "symbols",
+ "moji": "🈂",
+ "unicodeVersion": "6.0",
"digest": "6042bcabd1516ef3847d695aba22851c49421244432d256e24eba04e8a223dab"
},
- {
- "name": "sagittarius",
- "unicode": "2650",
+ "sagittarius": {
+ "category": "symbols",
+ "moji": "♐",
+ "unicodeVersion": "1.1",
"digest": "a02593e025023f2e82a01c587a8c0bbb1eff88cbcabf535a1558413eb32ed1d5"
},
- {
- "name": "sailboat",
- "unicode": "26F5",
+ "sailboat": {
+ "category": "travel",
+ "moji": "⛵",
+ "unicodeVersion": "5.2",
"digest": "c95ef4dc939cbdcb757ef3cd90331310e8c0a426add8cc800bae2540148a3195"
},
- {
- "name": "sake",
- "unicode": "1F376",
+ "sake": {
+ "category": "food",
+ "moji": "🍶",
+ "unicodeVersion": "6.0",
"digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4"
},
- {
- "name": "salad",
- "unicode": "1F957",
- "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
- },
- {
- "name": "green_salad",
- "unicode": "1F957",
+ "salad": {
+ "category": "food",
+ "moji": "🥗",
+ "unicodeVersion": "9.0",
"digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
},
- {
- "name": "sandal",
- "unicode": "1F461",
+ "sandal": {
+ "category": "people",
+ "moji": "👡",
+ "unicodeVersion": "6.0",
"digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d"
},
- {
- "name": "santa",
- "unicode": "1F385",
+ "santa": {
+ "category": "people",
+ "moji": "🎅",
+ "unicodeVersion": "6.0",
"digest": "178513e3d815917e59958870f5885b3414b43a16b8056980c863a468dfe00179"
},
- {
- "name": "santa_tone1",
- "unicode": "1F385-1F3FB",
+ "santa_tone1": {
+ "category": "people",
+ "moji": "🎅🏻",
+ "unicodeVersion": "8.0",
"digest": "bf900bbc19bbd329229add9326e28e8197b69d6ddceb69f42162b0200fde5d16"
},
- {
- "name": "santa_tone2",
- "unicode": "1F385-1F3FC",
+ "santa_tone2": {
+ "category": "people",
+ "moji": "🎅🏼",
+ "unicodeVersion": "8.0",
"digest": "7340f2171adab97198e3eecac8b0d84c4c2a41f84606301a0d10e9fe655c93d1"
},
- {
- "name": "santa_tone3",
- "unicode": "1F385-1F3FD",
+ "santa_tone3": {
+ "category": "people",
+ "moji": "🎅🏽",
+ "unicodeVersion": "8.0",
"digest": "7368ab75454ec28d8f7d6baef6ad69b5278445a9f50753f6624731bffde32054"
},
- {
- "name": "santa_tone4",
- "unicode": "1F385-1F3FE",
+ "santa_tone4": {
+ "category": "people",
+ "moji": "🎅🏾",
+ "unicodeVersion": "8.0",
"digest": "0ee60188353e0ee7772079c192bebbc6d49e74e63906f840c66da4eb35f4f245"
},
- {
- "name": "santa_tone5",
- "unicode": "1F385-1F3FF",
+ "santa_tone5": {
+ "category": "people",
+ "moji": "🎅🏿",
+ "unicodeVersion": "8.0",
"digest": "e4378a0cc5d21e9b9fe6e35c32d1ebc6fb8c2e1c09554cd096aeaefd3a6eb511"
},
- {
- "name": "satellite",
- "unicode": "1F4E1",
+ "satellite": {
+ "category": "objects",
+ "moji": "📡",
+ "unicodeVersion": "6.0",
"digest": "c9d63118dcb445856917bb080460ab695cc78e715dcbba30ba18dffa9e906b27"
},
- {
- "name": "satellite_orbital",
- "unicode": "1F6F0",
+ "satellite_orbital": {
+ "category": "travel",
+ "moji": "🛰",
+ "unicodeVersion": "7.0",
"digest": "beb2f50e7f2b010e76bed9daa95d7329a93c783d3ebc4f0b797dd721c5e3d32d"
},
- {
- "name": "saxophone",
- "unicode": "1F3B7",
+ "saxophone": {
+ "category": "activity",
+ "moji": "🎷",
+ "unicodeVersion": "6.0",
"digest": "dfd138634f6702a3b89b5a2a50016720eef3f800b0d1d8c9fe097808c9491e96"
},
- {
- "name": "scales",
- "unicode": "2696",
+ "scales": {
+ "category": "objects",
+ "moji": "⚖",
+ "unicodeVersion": "4.1",
"digest": "2280c026f16c6b92e0daa00bc14e718770f8d231c571ab439bde84d837cf31cc"
},
- {
- "name": "school",
- "unicode": "1F3EB",
+ "school": {
+ "category": "travel",
+ "moji": "🏫",
+ "unicodeVersion": "6.0",
"digest": "af198b068a86ccad3daec4c6873e6b4735086c1ecbb3848182e70bae9aa3ee24"
},
- {
- "name": "school_satchel",
- "unicode": "1F392",
+ "school_satchel": {
+ "category": "people",
+ "moji": "🎒",
+ "unicodeVersion": "6.0",
"digest": "f670ae8aea67eb9d8aaa0bf2748c1cc3e503dcc1dbe999133afcdf21af046b24"
},
- {
- "name": "scissors",
- "unicode": "2702",
+ "scissors": {
+ "category": "objects",
+ "moji": "✂",
+ "unicodeVersion": "1.1",
"digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8"
},
- {
- "name": "scooter",
- "unicode": "1F6F4",
+ "scooter": {
+ "category": "travel",
+ "moji": "🛴",
+ "unicodeVersion": "9.0",
"digest": "4a7db148880398db75e059711cb53edefb6b8fa9d442009f52856b887ab1dde4"
},
- {
- "name": "scorpion",
- "unicode": "1F982",
+ "scorpion": {
+ "category": "nature",
+ "moji": "🦂",
+ "unicodeVersion": "8.0",
"digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a"
},
- {
- "name": "scorpius",
- "unicode": "264F",
+ "scorpius": {
+ "category": "symbols",
+ "moji": "♏",
+ "unicodeVersion": "1.1",
"digest": "a36404b408814c2ecb8fa8b61f5c5432dfcf54cae8c09cc67b8d0fadf7cbdc03"
},
- {
- "name": "scream",
- "unicode": "1F631",
+ "scream": {
+ "category": "people",
+ "moji": "😱",
+ "unicodeVersion": "6.0",
"digest": "916e4903a4b694da4b00f190f872a4e100e7736b7a2e6171fa1636f46bf646e6"
},
- {
- "name": "scream_cat",
- "unicode": "1F640",
+ "scream_cat": {
+ "category": "people",
+ "moji": "🙀",
+ "unicodeVersion": "6.0",
"digest": "f1d3a6ff538064e7d5e0321bbc33aba44e8da703dc1894ef1403c0cd6d63d781"
},
- {
- "name": "scroll",
- "unicode": "1F4DC",
+ "scroll": {
+ "category": "objects",
+ "moji": "📜",
+ "unicodeVersion": "6.0",
"digest": "9b2cb00860bcc2d20017cafb2ed9681b6232dc07273d489d75d53ce29e4ba3ab"
},
- {
- "name": "seat",
- "unicode": "1F4BA",
+ "seat": {
+ "category": "travel",
+ "moji": "💺",
+ "unicodeVersion": "6.0",
"digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1"
},
- {
- "name": "second_place",
- "unicode": "1F948",
+ "second_place": {
+ "category": "activity",
+ "moji": "🥈",
+ "unicodeVersion": "9.0",
"digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
},
- {
- "name": "second_place_medal",
- "unicode": "1F948",
- "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
- },
- {
- "name": "secret",
- "unicode": "3299",
+ "secret": {
+ "category": "symbols",
+ "moji": "㊙",
+ "unicodeVersion": "1.1",
"digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0"
},
- {
- "name": "see_no_evil",
- "unicode": "1F648",
+ "see_no_evil": {
+ "category": "nature",
+ "moji": "🙈",
+ "unicodeVersion": "6.0",
"digest": "3ff66d2e84b36d071d0a34f8e41cfd620a56b83131474ea50ed7803b635551ed"
},
- {
- "name": "seedling",
- "unicode": "1F331",
+ "seedling": {
+ "category": "nature",
+ "moji": "🌱",
+ "unicodeVersion": "6.0",
"digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75"
},
- {
- "name": "selfie",
- "unicode": "1F933",
+ "selfie": {
+ "category": "people",
+ "moji": "🤳",
+ "unicodeVersion": "9.0",
"digest": "2a1bc9f18ad4d6fb893d91c88ef1b2d9bd063dc2bb1a4b08c248c30f52545d4e"
},
- {
- "name": "selfie_tone1",
- "unicode": "1F933-1F3FB",
+ "selfie_tone1": {
+ "category": "people",
+ "moji": "🤳🏻",
+ "unicodeVersion": "9.0",
"digest": "26dc212ffed30c276bd6a66a72bc4513e68098a2205fb4ca5b51ccfa1de5b544"
},
- {
- "name": "selfie_tone2",
- "unicode": "1F933-1F3FC",
+ "selfie_tone2": {
+ "category": "people",
+ "moji": "🤳🏼",
+ "unicodeVersion": "9.0",
"digest": "71eceaefda46e3521f374f76693e7fa8f215067498067900080e2925ca94d7de"
},
- {
- "name": "selfie_tone3",
- "unicode": "1F933-1F3FD",
+ "selfie_tone3": {
+ "category": "people",
+ "moji": "🤳🏽",
+ "unicodeVersion": "9.0",
"digest": "53eabbd4f6b8ebbd2f7af7bf5cd64309c4039ac1c5b2180290a547deaafcebdf"
},
- {
- "name": "selfie_tone4",
- "unicode": "1F933-1F3FE",
+ "selfie_tone4": {
+ "category": "people",
+ "moji": "🤳🏾",
+ "unicodeVersion": "9.0",
"digest": "0baad378b09652b99c5d458db2e03b4db14a1557db4ea0969806a0ca1d33d40c"
},
- {
- "name": "selfie_tone5",
- "unicode": "1F933-1F3FF",
+ "selfie_tone5": {
+ "category": "people",
+ "moji": "🤳🏿",
+ "unicodeVersion": "9.0",
"digest": "9a07608f34ec4dad48764a855f83f3965709d7b2fd2342e6dc9ed61f23f4adfd"
},
- {
- "name": "seven",
- "unicode": "0037-20E3",
+ "seven": {
+ "category": "symbols",
+ "moji": "7️⃣",
+ "unicodeVersion": "3.0",
"digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2"
},
- {
- "name": "shallow_pan_of_food",
- "unicode": "1F958",
+ "shallow_pan_of_food": {
+ "category": "food",
+ "moji": "🥘",
+ "unicodeVersion": "9.0",
"digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
},
- {
- "name": "paella",
- "unicode": "1F958",
- "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
- },
- {
- "name": "shamrock",
- "unicode": "2618",
+ "shamrock": {
+ "category": "nature",
+ "moji": "☘",
+ "unicodeVersion": "4.1",
"digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488"
},
- {
- "name": "shark",
- "unicode": "1F988",
+ "shark": {
+ "category": "nature",
+ "moji": "🦈",
+ "unicodeVersion": "9.0",
"digest": "23a2364b6356e7bbb84c138e9cf58e2c68cd8caabb337a0c4d365ce87bf5d2da"
},
- {
- "name": "shaved_ice",
- "unicode": "1F367",
+ "shaved_ice": {
+ "category": "food",
+ "moji": "🍧",
+ "unicodeVersion": "6.0",
"digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74"
},
- {
- "name": "sheep",
- "unicode": "1F411",
+ "sheep": {
+ "category": "nature",
+ "moji": "🐑",
+ "unicodeVersion": "6.0",
"digest": "c867c8e6e51768f1f51f4fe5abd3fbd5c1d69b01a3cb48b5fb94b6e2338a271c"
},
- {
- "name": "shell",
- "unicode": "1F41A",
+ "shell": {
+ "category": "nature",
+ "moji": "🐚",
+ "unicodeVersion": "6.0",
"digest": "8983652d33ad6ab91195518cecb5a268a1c0ae603d271f0ddd756ff50058ddb3"
},
- {
- "name": "shield",
- "unicode": "1F6E1",
+ "shield": {
+ "category": "objects",
+ "moji": "🛡",
+ "unicodeVersion": "7.0",
"digest": "763d0a56a62c51c730ccb0fbea38ab597cbf41a85ab968198e6ec35630d50aa5"
},
- {
- "name": "shinto_shrine",
- "unicode": "26E9",
+ "shinto_shrine": {
+ "category": "travel",
+ "moji": "⛩",
+ "unicodeVersion": "5.2",
"digest": "38a6d756c5aa9703510afa5076d75192f7814bbb6632394d4b8253d9ceda7f8c"
},
- {
- "name": "ship",
- "unicode": "1F6A2",
+ "ship": {
+ "category": "travel",
+ "moji": "🚢",
+ "unicodeVersion": "6.0",
"digest": "79c680845892a3e81ec6af2160ee07c29147155943e5daba6c76d04252014c20"
},
- {
- "name": "shirt",
- "unicode": "1F455",
+ "shirt": {
+ "category": "people",
+ "moji": "👕",
+ "unicodeVersion": "6.0",
"digest": "46c7253e15d7cac03699ddb1550fbb7565bbe487310f7e218c0583aa69f9d3c5"
},
- {
- "name": "shopping_bags",
- "unicode": "1F6CD",
+ "shopping_bags": {
+ "category": "objects",
+ "moji": "🛍",
+ "unicodeVersion": "7.0",
"digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b"
},
- {
- "name": "shopping_cart",
- "unicode": "1F6D2",
+ "shopping_cart": {
+ "category": "objects",
+ "moji": "🛒",
+ "unicodeVersion": "9.0",
"digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
},
- {
- "name": "shopping_trolley",
- "unicode": "1F6D2",
- "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
- },
- {
- "name": "shower",
- "unicode": "1F6BF",
+ "shower": {
+ "category": "objects",
+ "moji": "🚿",
+ "unicodeVersion": "6.0",
"digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01"
},
- {
- "name": "shrimp",
- "unicode": "1F990",
+ "shrimp": {
+ "category": "nature",
+ "moji": "🦐",
+ "unicodeVersion": "9.0",
"digest": "b3651f3be3767125076a013fe903854f5b456a8afae865cb219cf528e0f44caa"
},
- {
- "name": "shrug",
- "unicode": "1F937",
+ "shrug": {
+ "category": "people",
+ "moji": "🤷",
+ "unicodeVersion": "9.0",
"digest": "6e264243cc3b6e396069dea4357a958bdcd4081cb1af0ed6aa47235bef88cf27"
},
- {
- "name": "shrug_tone1",
- "unicode": "1F937-1F3FB",
+ "shrug_tone1": {
+ "category": "people",
+ "moji": "🤷🏻",
+ "unicodeVersion": "9.0",
"digest": "0567b9fd95c8a857914003a5465a500ca79c8111811d45b865021b1b1d92d0b1"
},
- {
- "name": "shrug_tone2",
- "unicode": "1F937-1F3FC",
+ "shrug_tone2": {
+ "category": "people",
+ "moji": "🤷🏼",
+ "unicodeVersion": "9.0",
"digest": "1557c2f5e3d4599c806d74c0b78afcca940678787534b6862bb89a20601bac8a"
},
- {
- "name": "shrug_tone3",
- "unicode": "1F937-1F3FD",
+ "shrug_tone3": {
+ "category": "people",
+ "moji": "🤷🏽",
+ "unicodeVersion": "9.0",
"digest": "f02754541a7bf74ba7eebe6c27daf1e3e1dac25172c35b8ba45641e278dfda3d"
},
- {
- "name": "shrug_tone4",
- "unicode": "1F937-1F3FE",
+ "shrug_tone4": {
+ "category": "people",
+ "moji": "🤷🏾",
+ "unicodeVersion": "9.0",
"digest": "2b5121164cb5f4e253d8fb31f6445cf8afaf30dba41732edc511440cdb78d15c"
},
- {
- "name": "shrug_tone5",
- "unicode": "1F937-1F3FF",
+ "shrug_tone5": {
+ "category": "people",
+ "moji": "🤷🏿",
+ "unicodeVersion": "9.0",
"digest": "62d99a26bbad479f574f66208c41b9960cd41fb9d79d3a13fbdaa44682077115"
},
- {
- "name": "signal_strength",
- "unicode": "1F4F6",
+ "signal_strength": {
+ "category": "symbols",
+ "moji": "📶",
+ "unicodeVersion": "6.0",
"digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447"
},
- {
- "name": "six",
- "unicode": "0036-20E3",
+ "six": {
+ "category": "symbols",
+ "moji": "6️⃣",
+ "unicodeVersion": "3.0",
"digest": "cede9324261208d0fd5d00fcdfc0df0331944bd9cff4f40b30a582a641526c1c"
},
- {
- "name": "six_pointed_star",
- "unicode": "1F52F",
+ "six_pointed_star": {
+ "category": "symbols",
+ "moji": "🔯",
+ "unicodeVersion": "6.0",
"digest": "9203e3b4f08af439ae0bfb6a7b29a02dceb027b6c2dc5463b524dfd314cbff4e"
},
- {
- "name": "ski",
- "unicode": "1F3BF",
+ "ski": {
+ "category": "activity",
+ "moji": "🎿",
+ "unicodeVersion": "6.0",
"digest": "80f0ca8660ba373fef823af9e98e148c4ddb1e217eb6d0a0ea2bae2288b57570"
},
- {
- "name": "skier",
- "unicode": "26F7",
+ "skier": {
+ "category": "activity",
+ "moji": "⛷",
+ "unicodeVersion": "5.2",
"digest": "4fff0aa155367f551a59aed9657b8afa159173882b25db9cd8434293d1eed76d"
},
- {
- "name": "skull",
- "unicode": "1F480",
- "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
- },
- {
- "name": "skeleton",
- "unicode": "1F480",
+ "skull": {
+ "category": "people",
+ "moji": "💀",
+ "unicodeVersion": "6.0",
"digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a"
},
- {
- "name": "skull_crossbones",
- "unicode": "2620",
+ "skull_crossbones": {
+ "category": "objects",
+ "moji": "☠",
+ "unicodeVersion": "1.1",
"digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
},
- {
- "name": "skull_and_crossbones",
- "unicode": "2620",
- "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123"
- },
- {
- "name": "sleeping",
- "unicode": "1F634",
+ "sleeping": {
+ "category": "people",
+ "moji": "😴",
+ "unicodeVersion": "6.1",
"digest": "1050a011509b56735c9f30a6fccc876256e2a4546dc6052e518151c8aca4b526"
},
- {
- "name": "sleeping_accommodation",
- "unicode": "1F6CC",
+ "sleeping_accommodation": {
+ "category": "objects",
+ "moji": "🛌",
+ "unicodeVersion": "7.0",
"digest": "2ce42c027d1d0947abc403c359fd668a7bc44f5ead2582e97f3db7dd4e22e5d5"
},
- {
- "name": "sleepy",
- "unicode": "1F62A",
+ "sleepy": {
+ "category": "people",
+ "moji": "😪",
+ "unicodeVersion": "6.0",
"digest": "2ee9bb1f72ef99e0e33095ec2bbf7a58ffea0ff7d40b840f4cdba57be9de74b0"
},
- {
- "name": "slight_frown",
- "unicode": "1F641",
- "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
- },
- {
- "name": "slightly_frowning_face",
- "unicode": "1F641",
+ "slight_frown": {
+ "category": "people",
+ "moji": "🙁",
+ "unicodeVersion": "7.0",
"digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9"
},
- {
- "name": "slight_smile",
- "unicode": "1F642",
- "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
- },
- {
- "name": "slightly_smiling_face",
- "unicode": "1F642",
+ "slight_smile": {
+ "category": "people",
+ "moji": "🙂",
+ "unicodeVersion": "7.0",
"digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe"
},
- {
- "name": "slot_machine",
- "unicode": "1F3B0",
+ "slot_machine": {
+ "category": "activity",
+ "moji": "🎰",
+ "unicodeVersion": "6.0",
"digest": "914184788f8cd865cd074dca25c22acee31f5498117bd9a6e78cae67e6601652"
},
- {
- "name": "small_blue_diamond",
- "unicode": "1F539",
+ "small_blue_diamond": {
+ "category": "symbols",
+ "moji": "🔹",
+ "unicodeVersion": "6.0",
"digest": "0b56d8e6b5ddf1f49fcc76e45e5fb2ee9f99ae6ffe682c26eaea4d9b7faac36c"
},
- {
- "name": "small_orange_diamond",
- "unicode": "1F538",
+ "small_orange_diamond": {
+ "category": "symbols",
+ "moji": "🔸",
+ "unicodeVersion": "6.0",
"digest": "a2235830550e289c1608f2dcf5ede48f5c1a0eff45570699c39708c9677ab950"
},
- {
- "name": "small_red_triangle",
- "unicode": "1F53A",
+ "small_red_triangle": {
+ "category": "symbols",
+ "moji": "🔺",
+ "unicodeVersion": "6.0",
"digest": "8c2985c4e9ce42d2f3b35539b879bc36206c5ef749f39fbd1eac51bd2676e1e5"
},
- {
- "name": "small_red_triangle_down",
- "unicode": "1F53B",
+ "small_red_triangle_down": {
+ "category": "symbols",
+ "moji": "🔻",
+ "unicodeVersion": "6.0",
"digest": "46bd328df2fbf5d0597596bbf00d2d5f6e0c65bcb8f3fb325df8ba0c25e445b5"
},
- {
- "name": "smile",
- "unicode": "1F604",
+ "smile": {
+ "category": "people",
+ "moji": "😄",
+ "unicodeVersion": "6.0",
"digest": "14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14"
},
- {
- "name": "smile_cat",
- "unicode": "1F638",
+ "smile_cat": {
+ "category": "people",
+ "moji": "😸",
+ "unicodeVersion": "6.0",
"digest": "c35b76d6df100edb4022d762f47abfeb9f5e70886960c1d25908bd5d57ccb47e"
},
- {
- "name": "smiley",
- "unicode": "1F603",
+ "smiley": {
+ "category": "people",
+ "moji": "😃",
+ "unicodeVersion": "6.0",
"digest": "a89f31eb9d814636852517a7f4eadec59195e2ac2cc9f8d124f1a1cc0f775b4a"
},
- {
- "name": "smiley_cat",
- "unicode": "1F63A",
+ "smiley_cat": {
+ "category": "people",
+ "moji": "😺",
+ "unicodeVersion": "6.0",
"digest": "3e66a113c5e3e73fb94be29084cb27986b6bdb0e78ab44785bf2a35a550e71bf"
},
- {
- "name": "smiling_imp",
- "unicode": "1F608",
+ "smiling_imp": {
+ "category": "people",
+ "moji": "😈",
+ "unicodeVersion": "6.0",
"digest": "3e02131d16525938f6facc7e097365dec7e13c8a0049a3be35fc29c80cc291b3"
},
- {
- "name": "smirk",
- "unicode": "1F60F",
+ "smirk": {
+ "category": "people",
+ "moji": "😏",
+ "unicodeVersion": "6.0",
"digest": "3c180d46f5574d6fca3bb68eb02517da60b7008843cb3e90f2f9620d0c8ee943"
},
- {
- "name": "smirk_cat",
- "unicode": "1F63C",
+ "smirk_cat": {
+ "category": "people",
+ "moji": "😼",
+ "unicodeVersion": "6.0",
"digest": "0683c7f73e1f65984e91313607d7cca21d99acd4b2e9932f00e0fffd0ce90742"
},
- {
- "name": "smoking",
- "unicode": "1F6AC",
+ "smoking": {
+ "category": "objects",
+ "moji": "🚬",
+ "unicodeVersion": "6.0",
"digest": "baa9cb444bf0fe5c74358f981b19bc9e5c0415ced7f042baf93642282476ea61"
},
- {
- "name": "snail",
- "unicode": "1F40C",
+ "snail": {
+ "category": "nature",
+ "moji": "🐌",
+ "unicodeVersion": "6.0",
"digest": "5733bf3672ae4b2b3e090fa670aeac70dcbcc04ca5b13abc8c8e53b8b3d4ff33"
},
- {
- "name": "snake",
- "unicode": "1F40D",
+ "snake": {
+ "category": "nature",
+ "moji": "🐍",
+ "unicodeVersion": "6.0",
"digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773"
},
- {
- "name": "sneezing_face",
- "unicode": "1F927",
- "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
- },
- {
- "name": "sneeze",
- "unicode": "1F927",
+ "sneezing_face": {
+ "category": "people",
+ "moji": "🤧",
+ "unicodeVersion": "9.0",
"digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
},
- {
- "name": "snowboarder",
- "unicode": "1F3C2",
+ "snowboarder": {
+ "category": "activity",
+ "moji": "🏂",
+ "unicodeVersion": "6.0",
"digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19"
},
- {
- "name": "snowflake",
- "unicode": "2744",
+ "snowflake": {
+ "category": "nature",
+ "moji": "❄",
+ "unicodeVersion": "1.1",
"digest": "6556c918e181df01ba849e76c43972d5310439971e5d8fc2409d112c05bf0028"
},
- {
- "name": "snowman",
- "unicode": "26C4",
+ "snowman": {
+ "category": "nature",
+ "moji": "⛄",
+ "unicodeVersion": "5.2",
"digest": "6137456b2335e88e09c1859615eb22bb636355ef438f7a3949ad2f3d54478dd3"
},
- {
- "name": "snowman2",
- "unicode": "2603",
+ "snowman2": {
+ "category": "nature",
+ "moji": "☃",
+ "unicodeVersion": "1.1",
"digest": "33ec75c22a13c81fa3c6b24a77ac1a08dc0dbe70b3716cf17b6702014d8a63fe"
},
- {
- "name": "sob",
- "unicode": "1F62D",
+ "sob": {
+ "category": "people",
+ "moji": "😭",
+ "unicodeVersion": "6.0",
"digest": "d1ed4b31861f9f9fd4e9c95a9c17530e2320a1b4cad6ececb1545ce25d65e4ce"
},
- {
- "name": "soccer",
- "unicode": "26BD",
+ "soccer": {
+ "category": "activity",
+ "moji": "⚽",
+ "unicodeVersion": "5.2",
"digest": "6a3f2e6a9a0b64c3fbf8705995792091daf386a4112dba75507a1f556f662f84"
},
- {
- "name": "soon",
- "unicode": "1F51C",
+ "soon": {
+ "category": "symbols",
+ "moji": "🔜",
+ "unicodeVersion": "6.0",
"digest": "a49d1bcfbac3e6ccc05b9a9863eff74b0eb8b4d4b22b8b0f7b2787fcba1c73cc"
},
- {
- "name": "sos",
- "unicode": "1F198",
+ "sos": {
+ "category": "symbols",
+ "moji": "🆘",
+ "unicodeVersion": "6.0",
"digest": "2fa7e0274383aeed6019eb9177e778d7aab8b88575b078b0ffeb77cd18df14b3"
},
- {
- "name": "sound",
- "unicode": "1F509",
+ "sound": {
+ "category": "symbols",
+ "moji": "🔉",
+ "unicodeVersion": "6.0",
"digest": "faaca7b315b2495cbc381468580d25f1d11362441c35bb43d8a914f2ec8202d2"
},
- {
- "name": "space_invader",
- "unicode": "1F47E",
+ "space_invader": {
+ "category": "activity",
+ "moji": "👾",
+ "unicodeVersion": "6.0",
"digest": "e75379cb5063f9a8861d762ad1886097c1697fbb61f2e4e8f531047955a4a2dd"
},
- {
- "name": "spades",
- "unicode": "2660",
+ "spades": {
+ "category": "symbols",
+ "moji": "♠",
+ "unicodeVersion": "1.1",
"digest": "2c4d20f6a4893cfc62498d3f1f8f67577f39ed09f3e6682d8cb9cd8f365d30da"
},
- {
- "name": "spaghetti",
- "unicode": "1F35D",
+ "spaghetti": {
+ "category": "food",
+ "moji": "🍝",
+ "unicodeVersion": "6.0",
"digest": "6d3451dc0faa1913539edb99261448f51735f269b61193c53dfe63466c0191e8"
},
- {
- "name": "sparkle",
- "unicode": "2747",
+ "sparkle": {
+ "category": "symbols",
+ "moji": "❇",
+ "unicodeVersion": "1.1",
"digest": "7131163cd6c2f879110c86e9f068c33cf580f7c4b619449c41851fe6083402ee"
},
- {
- "name": "sparkler",
- "unicode": "1F387",
+ "sparkler": {
+ "category": "travel",
+ "moji": "🎇",
+ "unicodeVersion": "6.0",
"digest": "88539ed8a13bd66e0c265c0913bd3ec2ddc4d95484323595713beb102221a1f6"
},
- {
- "name": "sparkles",
- "unicode": "2728",
+ "sparkles": {
+ "category": "nature",
+ "moji": "✨",
+ "unicodeVersion": "6.0",
"digest": "cf84d16b1c0a381d5a7ae79031872747c9a6887eab6e92cc4a10a4b8600ef506"
},
- {
- "name": "sparkling_heart",
- "unicode": "1F496",
+ "sparkling_heart": {
+ "category": "symbols",
+ "moji": "💖",
+ "unicodeVersion": "6.0",
"digest": "b80b1ddef83b6528b309a194f6f2faf5acab603daeb9254523efc2b941bcb6d2"
},
- {
- "name": "speak_no_evil",
- "unicode": "1F64A",
+ "speak_no_evil": {
+ "category": "nature",
+ "moji": "🙊",
+ "unicodeVersion": "6.0",
"digest": "d2d7cfb4d471928a496bdc146890adc8422a68500b68115630b24c125d18e81f"
},
- {
- "name": "speaker",
- "unicode": "1F508",
+ "speaker": {
+ "category": "symbols",
+ "moji": "🔈",
+ "unicodeVersion": "6.0",
"digest": "dbca5f7181728d2ad67ff76fd566ffbdf53e333e7eeed341f54668bd47969413"
},
- {
- "name": "speaking_head",
- "unicode": "1F5E3",
+ "speaking_head": {
+ "category": "people",
+ "moji": "🗣",
+ "unicodeVersion": "7.0",
"digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
},
- {
- "name": "speaking_head_in_silhouette",
- "unicode": "1F5E3",
- "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544"
- },
- {
- "name": "speech_balloon",
- "unicode": "1F4AC",
+ "speech_balloon": {
+ "category": "symbols",
+ "moji": "💬",
+ "unicodeVersion": "6.0",
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
- {
- "name": "speedboat",
- "unicode": "1F6A4",
+ "speedboat": {
+ "category": "travel",
+ "moji": "🚤",
+ "unicodeVersion": "6.0",
"digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576"
},
- {
- "name": "spider",
- "unicode": "1F577",
+ "spider": {
+ "category": "nature",
+ "moji": "🕷",
+ "unicodeVersion": "7.0",
"digest": "8411eac0c1b80926fd93cc1d6423e00b05d04c485b79ee232da8f1714e899a37"
},
- {
- "name": "spider_web",
- "unicode": "1F578",
+ "spider_web": {
+ "category": "nature",
+ "moji": "🕸",
+ "unicodeVersion": "7.0",
"digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23"
},
- {
- "name": "spoon",
- "unicode": "1F944",
+ "spoon": {
+ "category": "food",
+ "moji": "🥄",
+ "unicodeVersion": "9.0",
"digest": "4fa31d59e5bffd2c45a8e01fcd5652e78a5691cbfa744e69882bc67173ddea05"
},
- {
- "name": "spy",
- "unicode": "1F575",
- "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
- },
- {
- "name": "sleuth_or_spy",
- "unicode": "1F575",
+ "spy": {
+ "category": "people",
+ "moji": "🕵",
+ "unicodeVersion": "7.0",
"digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
},
- {
- "name": "spy_tone1",
- "unicode": "1F575-1F3FB",
- "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
- },
- {
- "name": "sleuth_or_spy_tone1",
- "unicode": "1F575-1F3FB",
+ "spy_tone1": {
+ "category": "people",
+ "moji": "🕵🏻",
+ "unicodeVersion": "8.0",
"digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2"
},
- {
- "name": "spy_tone2",
- "unicode": "1F575-1F3FC",
+ "spy_tone2": {
+ "category": "people",
+ "moji": "🕵🏼",
+ "unicodeVersion": "8.0",
"digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
},
- {
- "name": "sleuth_or_spy_tone2",
- "unicode": "1F575-1F3FC",
- "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68"
- },
- {
- "name": "spy_tone3",
- "unicode": "1F575-1F3FD",
- "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
- },
- {
- "name": "sleuth_or_spy_tone3",
- "unicode": "1F575-1F3FD",
+ "spy_tone3": {
+ "category": "people",
+ "moji": "🕵🏽",
+ "unicodeVersion": "8.0",
"digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df"
},
- {
- "name": "spy_tone4",
- "unicode": "1F575-1F3FE",
+ "spy_tone4": {
+ "category": "people",
+ "moji": "🕵🏾",
+ "unicodeVersion": "8.0",
"digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
},
- {
- "name": "sleuth_or_spy_tone4",
- "unicode": "1F575-1F3FE",
- "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3"
- },
- {
- "name": "spy_tone5",
- "unicode": "1F575-1F3FF",
- "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
- },
- {
- "name": "sleuth_or_spy_tone5",
- "unicode": "1F575-1F3FF",
+ "spy_tone5": {
+ "category": "people",
+ "moji": "🕵🏿",
+ "unicodeVersion": "8.0",
"digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
},
- {
- "name": "squid",
- "unicode": "1F991",
+ "squid": {
+ "category": "nature",
+ "moji": "🦑",
+ "unicodeVersion": "9.0",
"digest": "65a1b318c2c506b9d26cfd8282a5cf9922109595c8d12e92c3f7481ac7c08c49"
},
- {
- "name": "stadium",
- "unicode": "1F3DF",
+ "stadium": {
+ "category": "travel",
+ "moji": "🏟",
+ "unicodeVersion": "7.0",
"digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f"
},
- {
- "name": "star",
- "unicode": "2B50",
+ "star": {
+ "category": "nature",
+ "moji": "⭐",
+ "unicodeVersion": "5.1",
"digest": "d78e5c1b78caed103e100150c10b08a9ca3ee30c243943d6fc3cc08f422122e9"
},
- {
- "name": "star2",
- "unicode": "1F31F",
+ "star2": {
+ "category": "nature",
+ "moji": "🌟",
+ "unicodeVersion": "6.0",
"digest": "f91ac4afe3f5d4a52847ae8b4a9704b591e00399aebba553d150d7e34ee939fa"
},
- {
- "name": "star_and_crescent",
- "unicode": "262A",
+ "star_and_crescent": {
+ "category": "symbols",
+ "moji": "☪",
+ "unicodeVersion": "1.1",
"digest": "1bf3d29e50034f5e7c0dccff0a3a533b74bfa9b489e357b2739a473311f1332a"
},
- {
- "name": "star_of_david",
- "unicode": "2721",
+ "star_of_david": {
+ "category": "symbols",
+ "moji": "✡",
+ "unicodeVersion": "1.1",
"digest": "28a0bd0eeac9d0835ceb8425d72c2472464e863dd09b76a0ddc1c08cf1986402"
},
- {
- "name": "stars",
- "unicode": "1F320",
+ "stars": {
+ "category": "travel",
+ "moji": "🌠",
+ "unicodeVersion": "6.0",
"digest": "837d9045316b8fb5e533457eac61241534f641eb78d8cb75f688f80fb8e8a7f0"
},
- {
- "name": "station",
- "unicode": "1F689",
+ "station": {
+ "category": "travel",
+ "moji": "🚉",
+ "unicodeVersion": "6.0",
"digest": "27a163ac0aea4ed247a121cae826eafc475977c68b0d888e9405bea14326ff56"
},
- {
- "name": "statue_of_liberty",
- "unicode": "1F5FD",
+ "statue_of_liberty": {
+ "category": "travel",
+ "moji": "🗽",
+ "unicodeVersion": "6.0",
"digest": "f5a43599ab3f24ed3a78a745e06e2ac3e33107a292386ad81c67935ee5b22493"
},
- {
- "name": "steam_locomotive",
- "unicode": "1F682",
+ "steam_locomotive": {
+ "category": "travel",
+ "moji": "🚂",
+ "unicodeVersion": "6.0",
"digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba"
},
- {
- "name": "stew",
- "unicode": "1F372",
+ "stew": {
+ "category": "food",
+ "moji": "🍲",
+ "unicodeVersion": "6.0",
"digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df"
},
- {
- "name": "stop_button",
- "unicode": "23F9",
+ "stop_button": {
+ "category": "symbols",
+ "moji": "⏹",
+ "unicodeVersion": "7.0",
"digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a"
},
- {
- "name": "stopwatch",
- "unicode": "23F1",
+ "stopwatch": {
+ "category": "objects",
+ "moji": "⏱",
+ "unicodeVersion": "6.0",
"digest": "9b6b9491a24d8ab4f896eb876da7973f028bd5e7c51a3767ba7e61bb6fbb2be0"
},
- {
- "name": "straight_ruler",
- "unicode": "1F4CF",
+ "straight_ruler": {
+ "category": "objects",
+ "moji": "📏",
+ "unicodeVersion": "6.0",
"digest": "cee31101767bd3f961363599924dc3790675d05a1285a8396428d2f91771c111"
},
- {
- "name": "strawberry",
- "unicode": "1F353",
+ "strawberry": {
+ "category": "food",
+ "moji": "🍓",
+ "unicodeVersion": "6.0",
"digest": "5750a15e12f21259286ddbc3a8222a385b3b97a9f368897f42dd000060343174"
},
- {
- "name": "stuck_out_tongue",
- "unicode": "1F61B",
+ "stuck_out_tongue": {
+ "category": "people",
+ "moji": "😛",
+ "unicodeVersion": "6.1",
"digest": "92dc42980a6dfdd7204fc874a762d6a0bbf0fdbfb5a7c0698fca04782e99fde6"
},
- {
- "name": "stuck_out_tongue_closed_eyes",
- "unicode": "1F61D",
+ "stuck_out_tongue_closed_eyes": {
+ "category": "people",
+ "moji": "😝",
+ "unicodeVersion": "6.0",
"digest": "434d25ac24cad7ba699eae876a25d9a99b584449cca50b124bf6aa7f20a83d51"
},
- {
- "name": "stuck_out_tongue_winking_eye",
- "unicode": "1F61C",
+ "stuck_out_tongue_winking_eye": {
+ "category": "people",
+ "moji": "😜",
+ "unicodeVersion": "6.0",
"digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d"
},
- {
- "name": "stuffed_flatbread",
- "unicode": "1F959",
+ "stuffed_flatbread": {
+ "category": "food",
+ "moji": "🥙",
+ "unicodeVersion": "9.0",
"digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
},
- {
- "name": "stuffed_pita",
- "unicode": "1F959",
- "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
- },
- {
- "name": "sun_with_face",
- "unicode": "1F31E",
+ "sun_with_face": {
+ "category": "nature",
+ "moji": "🌞",
+ "unicodeVersion": "6.0",
"digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be"
},
- {
- "name": "sunflower",
- "unicode": "1F33B",
+ "sunflower": {
+ "category": "nature",
+ "moji": "🌻",
+ "unicodeVersion": "6.0",
"digest": "27d1161f50f932a6b26c404cf2e8f7083683ed0f2382d62b7472acccaa6eb695"
},
- {
- "name": "sunglasses",
- "unicode": "1F60E",
+ "sunglasses": {
+ "category": "people",
+ "moji": "😎",
+ "unicodeVersion": "6.0",
"digest": "966684382e5c59e98319e4c0ea7c304c61c2638ad5408faa49ce2c83c4416757"
},
- {
- "name": "sunny",
- "unicode": "2600",
+ "sunny": {
+ "category": "nature",
+ "moji": "☀",
+ "unicodeVersion": "1.1",
"digest": "460fea4cbbdd1595450c1033a2ee5de7fea2e2f147822efa49f7e204812415aa"
},
- {
- "name": "sunrise",
- "unicode": "1F305",
+ "sunrise": {
+ "category": "travel",
+ "moji": "🌅",
+ "unicodeVersion": "6.0",
"digest": "7718a49636b0cdd1862ed67c7a9d6e72f471c2591ff0d912485b1be55d1ea115"
},
- {
- "name": "sunrise_over_mountains",
- "unicode": "1F304",
+ "sunrise_over_mountains": {
+ "category": "travel",
+ "moji": "🌄",
+ "unicodeVersion": "6.0",
"digest": "743d0701cdbe2a814962363813c3153d3c5e62c3e410349f56d49dbb9581f356"
},
- {
- "name": "surfer",
- "unicode": "1F3C4",
+ "surfer": {
+ "category": "activity",
+ "moji": "🏄",
+ "unicodeVersion": "6.0",
"digest": "bb440775e9213430942015c37db8de58b5a561ee971b2a0f3993fc3f1d2554d4"
},
- {
- "name": "surfer_tone1",
- "unicode": "1F3C4-1F3FB",
+ "surfer_tone1": {
+ "category": "activity",
+ "moji": "🏄🏻",
+ "unicodeVersion": "8.0",
"digest": "a4937b030aca30b68bb644f37cf63c38aebce3c00b57d1c8a0ffe596b57d2f1e"
},
- {
- "name": "surfer_tone2",
- "unicode": "1F3C4-1F3FC",
+ "surfer_tone2": {
+ "category": "activity",
+ "moji": "🏄🏼",
+ "unicodeVersion": "8.0",
"digest": "1c2a954a9c5284dedf0327d6f3c954c9fdd3953b848076d298874775ad8bf0a3"
},
- {
- "name": "surfer_tone3",
- "unicode": "1F3C4-1F3FD",
+ "surfer_tone3": {
+ "category": "activity",
+ "moji": "🏄🏽",
+ "unicodeVersion": "8.0",
"digest": "418a3408b9ab026124f067c8597b500217e56bc28d9844a29eea5eee6f604ff8"
},
- {
- "name": "surfer_tone4",
- "unicode": "1F3C4-1F3FE",
+ "surfer_tone4": {
+ "category": "activity",
+ "moji": "🏄🏾",
+ "unicodeVersion": "8.0",
"digest": "530870b9ac9f4d45ff750e264feb90b44fb93ca2852f323987b06f5f12fb5a4d"
},
- {
- "name": "surfer_tone5",
- "unicode": "1F3C4-1F3FF",
+ "surfer_tone5": {
+ "category": "activity",
+ "moji": "🏄🏿",
+ "unicodeVersion": "8.0",
"digest": "40e11b1ae652cfd085d083377f1da24160065ed1b67403c6fa4655e6e44169ec"
},
- {
- "name": "sushi",
- "unicode": "1F363",
+ "sushi": {
+ "category": "food",
+ "moji": "🍣",
+ "unicodeVersion": "6.0",
"digest": "b924c621236ca3284b349b0509ae1043f2fc2c7f6d67615716f9717ada78c992"
},
- {
- "name": "suspension_railway",
- "unicode": "1F69F",
+ "suspension_railway": {
+ "category": "travel",
+ "moji": "🚟",
+ "unicodeVersion": "6.0",
"digest": "cd3d21da79864f0c018b863e82fb0561fff3c5e3c065303cfcb89c3663d638ba"
},
- {
- "name": "sweat",
- "unicode": "1F613",
+ "sweat": {
+ "category": "people",
+ "moji": "😓",
+ "unicodeVersion": "6.0",
"digest": "1aa771479aa1ac5eeea4bafbe93ebd85a0f692f6d869034f31e25b689c2e264d"
},
- {
- "name": "sweat_drops",
- "unicode": "1F4A6",
+ "sweat_drops": {
+ "category": "nature",
+ "moji": "💦",
+ "unicodeVersion": "6.0",
"digest": "b575b85415bc9852cf6415d417ebf799167fde03c6819ebcaa24ae1b3dde8dab"
},
- {
- "name": "sweat_smile",
- "unicode": "1F605",
+ "sweat_smile": {
+ "category": "people",
+ "moji": "😅",
+ "unicodeVersion": "6.0",
"digest": "171b0d0845d46c33bedb6d3b39fb1ff366e22ba90685eedabebd91bb2b0680de"
},
- {
- "name": "sweet_potato",
- "unicode": "1F360",
+ "sweet_potato": {
+ "category": "food",
+ "moji": "🍠",
+ "unicodeVersion": "6.0",
"digest": "4b91920f0b87d42763313bc476f4c821a74e4c12dc1c92165a859dddeaaf8844"
},
- {
- "name": "swimmer",
- "unicode": "1F3CA",
+ "swimmer": {
+ "category": "activity",
+ "moji": "🏊",
+ "unicodeVersion": "6.0",
"digest": "2c4ed4a51aad99d9957ae11a219d5164db9748fc3a65002c6085a9f15adfa9e2"
},
- {
- "name": "swimmer_tone1",
- "unicode": "1F3CA-1F3FB",
+ "swimmer_tone1": {
+ "category": "activity",
+ "moji": "🏊🏻",
+ "unicodeVersion": "8.0",
"digest": "48588f129ee4af52ca2e0f4594213391978601087cd607896b2f979ca077284b"
},
- {
- "name": "swimmer_tone2",
- "unicode": "1F3CA-1F3FC",
+ "swimmer_tone2": {
+ "category": "activity",
+ "moji": "🏊🏼",
+ "unicodeVersion": "8.0",
"digest": "fff209448524bd1ef4d6decabf6c1ead94c8d3d5b1bfb5e54f20cc8e139232fc"
},
- {
- "name": "swimmer_tone3",
- "unicode": "1F3CA-1F3FD",
+ "swimmer_tone3": {
+ "category": "activity",
+ "moji": "🏊🏽",
+ "unicodeVersion": "8.0",
"digest": "2003932cb2cf4ae9a10b23338bf375a9293fb18c0ecf91bdfae73be6eebb3800"
},
- {
- "name": "swimmer_tone4",
- "unicode": "1F3CA-1F3FE",
+ "swimmer_tone4": {
+ "category": "activity",
+ "moji": "🏊🏾",
+ "unicodeVersion": "8.0",
"digest": "20b4bff9baa1c694ad98067dde834c56092f023b9664bec382c2e512232bd480"
},
- {
- "name": "swimmer_tone5",
- "unicode": "1F3CA-1F3FF",
+ "swimmer_tone5": {
+ "category": "activity",
+ "moji": "🏊🏿",
+ "unicodeVersion": "8.0",
"digest": "0ff8eb57c2be8e80a1bc6ba75b8d9ffb9bd8d3be636150c4c03399ec1886f218"
},
- {
- "name": "symbols",
- "unicode": "1F523",
+ "symbols": {
+ "category": "symbols",
+ "moji": "🔣",
+ "unicodeVersion": "6.0",
"digest": "2a2a79816c4d0751a0d73586eec5e63b410653d3c85cc968906bf1fc03d89b94"
},
- {
- "name": "synagogue",
- "unicode": "1F54D",
+ "synagogue": {
+ "category": "travel",
+ "moji": "🕍",
+ "unicodeVersion": "8.0",
"digest": "98569cdd7c61528963b67b7891dfa46025c5e810cbb22ee18ddb3bd85de2da69"
},
- {
- "name": "syringe",
- "unicode": "1F489",
+ "syringe": {
+ "category": "objects",
+ "moji": "💉",
+ "unicodeVersion": "6.0",
"digest": "e1538e645ccc571227c994b71b3d1be2c4d072d8bd9c944a42ff4a11c91a34a6"
},
- {
- "name": "taco",
- "unicode": "1F32E",
+ "taco": {
+ "category": "food",
+ "moji": "🌮",
+ "unicodeVersion": "8.0",
"digest": "e1e45aefdb7445faeae75c3831df6a3d6f2590fcdd48a20d847593c246df613b"
},
- {
- "name": "tada",
- "unicode": "1F389",
+ "tada": {
+ "category": "objects",
+ "moji": "🎉",
+ "unicodeVersion": "6.0",
"digest": "1d2e6cbb2a3244240bc70209715d2213d1efee2e370cccfbcc046c333ae2d650"
},
- {
- "name": "tanabata_tree",
- "unicode": "1F38B",
+ "tanabata_tree": {
+ "category": "nature",
+ "moji": "🎋",
+ "unicodeVersion": "6.0",
"digest": "592f2907ffc1b914390e1a106c15120ff3607e99192158b94d237975647c5540"
},
- {
- "name": "tangerine",
- "unicode": "1F34A",
+ "tangerine": {
+ "category": "food",
+ "moji": "🍊",
+ "unicodeVersion": "6.0",
"digest": "40c9ddcde1b0bcfaeb466629a87825eb8c2037835720cbee5e2fda04be3c8d0a"
},
- {
- "name": "taurus",
- "unicode": "2649",
+ "taurus": {
+ "category": "symbols",
+ "moji": "♉",
+ "unicodeVersion": "1.1",
"digest": "21cf24cb6410ab6596e2df8b3e242cc07f9dbb247eabc00c590fe184b373d068"
},
- {
- "name": "taxi",
- "unicode": "1F695",
+ "taxi": {
+ "category": "travel",
+ "moji": "🚕",
+ "unicodeVersion": "6.0",
"digest": "c546cc743831cfbf0c15452767cf2a4faf3775066797e997ae7c1fcbe4eca479"
},
- {
- "name": "tea",
- "unicode": "1F375",
+ "tea": {
+ "category": "food",
+ "moji": "🍵",
+ "unicodeVersion": "6.0",
"digest": "00e3f1e389fa58c4fcd8c53ebbf83d25872f4315845ab1984b35410ae65553d9"
},
- {
- "name": "telephone",
- "unicode": "260E",
+ "telephone": {
+ "category": "objects",
+ "moji": "☎",
+ "unicodeVersion": "1.1",
"digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7"
},
- {
- "name": "telephone_receiver",
- "unicode": "1F4DE",
+ "telephone_receiver": {
+ "category": "objects",
+ "moji": "📞",
+ "unicodeVersion": "6.0",
"digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046"
},
- {
- "name": "telescope",
- "unicode": "1F52D",
+ "telescope": {
+ "category": "objects",
+ "moji": "🔭",
+ "unicodeVersion": "6.0",
"digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495"
},
- {
- "name": "ten",
- "unicode": "1F51F",
+ "ten": {
+ "category": "symbols",
+ "moji": "🔟",
+ "unicodeVersion": "6.0",
"digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40"
},
- {
- "name": "tennis",
- "unicode": "1F3BE",
+ "tennis": {
+ "category": "activity",
+ "moji": "🎾",
+ "unicodeVersion": "6.0",
"digest": "dc1600b4d8dce3d26259eb0d1c6ab042566565e3c1f2c96112210f1550a716fd"
},
- {
- "name": "tent",
- "unicode": "26FA",
+ "tent": {
+ "category": "travel",
+ "moji": "⛺",
+ "unicodeVersion": "5.2",
"digest": "30d9b17ac3219d4970ddf54d7c1a288b0ae50f7f3b82ed232c0b1b19ef585662"
},
- {
- "name": "thermometer",
- "unicode": "1F321",
+ "thermometer": {
+ "category": "objects",
+ "moji": "🌡",
+ "unicodeVersion": "7.0",
"digest": "66616babbcaef256d7b652796c760e8e893cb950c073348a408fe70904f80f25"
},
- {
- "name": "thermometer_face",
- "unicode": "1F912",
- "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
- },
- {
- "name": "face_with_thermometer",
- "unicode": "1F912",
+ "thermometer_face": {
+ "category": "people",
+ "moji": "🤒",
+ "unicodeVersion": "8.0",
"digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126"
},
- {
- "name": "thinking",
- "unicode": "1F914",
+ "thinking": {
+ "category": "people",
+ "moji": "🤔",
+ "unicodeVersion": "8.0",
"digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
},
- {
- "name": "thinking_face",
- "unicode": "1F914",
- "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
- },
- {
- "name": "third_place",
- "unicode": "1F949",
- "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
- },
- {
- "name": "third_place_medal",
- "unicode": "1F949",
+ "third_place": {
+ "category": "activity",
+ "moji": "🥉",
+ "unicodeVersion": "9.0",
"digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
},
- {
- "name": "thought_balloon",
- "unicode": "1F4AD",
+ "thought_balloon": {
+ "category": "symbols",
+ "moji": "💭",
+ "unicodeVersion": "6.0",
"digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e"
},
- {
- "name": "three",
- "unicode": "0033-20E3",
+ "three": {
+ "category": "symbols",
+ "moji": "3️⃣",
+ "unicodeVersion": "3.0",
"digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6"
},
- {
- "name": "thumbsdown",
- "unicode": "1F44E",
+ "thumbsdown": {
+ "category": "people",
+ "moji": "👎",
+ "unicodeVersion": "6.0",
"digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
},
- {
- "name": "-1",
- "unicode": "1F44E",
- "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
- },
- {
- "name": "thumbsdown_tone1",
- "unicode": "1F44E-1F3FB",
- "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
- },
- {
- "name": "-1_tone1",
- "unicode": "1F44E-1F3FB",
+ "thumbsdown_tone1": {
+ "category": "people",
+ "moji": "👎🏻",
+ "unicodeVersion": "8.0",
"digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3"
},
- {
- "name": "thumbsdown_tone2",
- "unicode": "1F44E-1F3FC",
+ "thumbsdown_tone2": {
+ "category": "people",
+ "moji": "👎🏼",
+ "unicodeVersion": "8.0",
"digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
},
- {
- "name": "-1_tone2",
- "unicode": "1F44E-1F3FC",
- "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507"
- },
- {
- "name": "thumbsdown_tone3",
- "unicode": "1F44E-1F3FD",
- "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
- },
- {
- "name": "-1_tone3",
- "unicode": "1F44E-1F3FD",
+ "thumbsdown_tone3": {
+ "category": "people",
+ "moji": "👎🏽",
+ "unicodeVersion": "8.0",
"digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe"
},
- {
- "name": "thumbsdown_tone4",
- "unicode": "1F44E-1F3FE",
+ "thumbsdown_tone4": {
+ "category": "people",
+ "moji": "👎🏾",
+ "unicodeVersion": "8.0",
"digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
},
- {
- "name": "-1_tone4",
- "unicode": "1F44E-1F3FE",
- "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44"
- },
- {
- "name": "thumbsdown_tone5",
- "unicode": "1F44E-1F3FF",
- "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
- },
- {
- "name": "-1_tone5",
- "unicode": "1F44E-1F3FF",
+ "thumbsdown_tone5": {
+ "category": "people",
+ "moji": "👎🏿",
+ "unicodeVersion": "8.0",
"digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58"
},
- {
- "name": "thumbsup",
- "unicode": "1F44D",
+ "thumbsup": {
+ "category": "people",
+ "moji": "👍",
+ "unicodeVersion": "6.0",
"digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
},
- {
- "name": "+1",
- "unicode": "1F44D",
- "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61"
- },
- {
- "name": "thumbsup_tone1",
- "unicode": "1F44D-1F3FB",
+ "thumbsup_tone1": {
+ "category": "people",
+ "moji": "👍🏻",
+ "unicodeVersion": "8.0",
"digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
},
- {
- "name": "+1_tone1",
- "unicode": "1F44D-1F3FB",
- "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21"
- },
- {
- "name": "thumbsup_tone2",
- "unicode": "1F44D-1F3FC",
+ "thumbsup_tone2": {
+ "category": "people",
+ "moji": "👍🏼",
+ "unicodeVersion": "8.0",
"digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
},
- {
- "name": "+1_tone2",
- "unicode": "1F44D-1F3FC",
- "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1"
- },
- {
- "name": "thumbsup_tone3",
- "unicode": "1F44D-1F3FD",
- "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
- },
- {
- "name": "+1_tone3",
- "unicode": "1F44D-1F3FD",
+ "thumbsup_tone3": {
+ "category": "people",
+ "moji": "👍🏽",
+ "unicodeVersion": "8.0",
"digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e"
},
- {
- "name": "thumbsup_tone4",
- "unicode": "1F44D-1F3FE",
+ "thumbsup_tone4": {
+ "category": "people",
+ "moji": "👍🏾",
+ "unicodeVersion": "8.0",
"digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
},
- {
- "name": "+1_tone4",
- "unicode": "1F44D-1F3FE",
- "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6"
- },
- {
- "name": "thumbsup_tone5",
- "unicode": "1F44D-1F3FF",
- "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
- },
- {
- "name": "+1_tone5",
- "unicode": "1F44D-1F3FF",
+ "thumbsup_tone5": {
+ "category": "people",
+ "moji": "👍🏿",
+ "unicodeVersion": "8.0",
"digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343"
},
- {
- "name": "thunder_cloud_rain",
- "unicode": "26C8",
- "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
- },
- {
- "name": "thunder_cloud_and_rain",
- "unicode": "26C8",
+ "thunder_cloud_rain": {
+ "category": "nature",
+ "moji": "⛈",
+ "unicodeVersion": "5.2",
"digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d"
},
- {
- "name": "ticket",
- "unicode": "1F3AB",
+ "ticket": {
+ "category": "activity",
+ "moji": "🎫",
+ "unicodeVersion": "6.0",
"digest": "b4326fe7761940216e6c76ee2928110a6b37bf913da9d694e96557e7c7c10420"
},
- {
- "name": "tickets",
- "unicode": "1F39F",
- "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
- },
- {
- "name": "admission_tickets",
- "unicode": "1F39F",
+ "tickets": {
+ "category": "activity",
+ "moji": "🎟",
+ "unicodeVersion": "7.0",
"digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a"
},
- {
- "name": "tiger",
- "unicode": "1F42F",
+ "tiger": {
+ "category": "nature",
+ "moji": "🐯",
+ "unicodeVersion": "6.0",
"digest": "e139531e6c930bc46242dc0ed274661229de026b5419d8ea8f99fdb0f8a719ab"
},
- {
- "name": "tiger2",
- "unicode": "1F405",
+ "tiger2": {
+ "category": "nature",
+ "moji": "🐅",
+ "unicodeVersion": "6.0",
"digest": "f930cc8714198310d9b0edca6baff243ac5a3320f75fadb56fa5acc6fe34ff24"
},
- {
- "name": "timer",
- "unicode": "23F2",
+ "timer": {
+ "category": "objects",
+ "moji": "⏲",
+ "unicodeVersion": "6.0",
"digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
},
- {
- "name": "timer_clock",
- "unicode": "23F2",
- "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0"
- },
- {
- "name": "tired_face",
- "unicode": "1F62B",
+ "tired_face": {
+ "category": "people",
+ "moji": "😫",
+ "unicodeVersion": "6.0",
"digest": "775739bc9324517e614878ca0960d793df97775feeb62b14dbfb311a42a21802"
},
- {
- "name": "tm",
- "unicode": "2122",
+ "tm": {
+ "category": "symbols",
+ "moji": "™",
+ "unicodeVersion": "1.1",
"digest": "7d9fafdb72d91860478fc185719f289f359eab2c368a132cb936a269e2ab6a24"
},
- {
- "name": "toilet",
- "unicode": "1F6BD",
+ "toilet": {
+ "category": "objects",
+ "moji": "🚽",
+ "unicodeVersion": "6.0",
"digest": "0d1b0dd0078f51104e8632a0726e1b3f075561a1ffa8a2546602de15798415d0"
},
- {
- "name": "tokyo_tower",
- "unicode": "1F5FC",
+ "tokyo_tower": {
+ "category": "travel",
+ "moji": "🗼",
+ "unicodeVersion": "6.0",
"digest": "73eaf6fd59d16396673afef620c6d928857d5cf616e95a40eaf2861686e0956a"
},
- {
- "name": "tomato",
- "unicode": "1F345",
+ "tomato": {
+ "category": "food",
+ "moji": "🍅",
+ "unicodeVersion": "6.0",
"digest": "d092d8ad381d542e59b6a82b4f1ef0d10fc1ed48460952375c6c5c6258cea111"
},
- {
- "name": "tone1",
- "unicode": "1F3FB",
+ "tone1": {
+ "category": "modifier",
+ "moji": "🏻",
+ "unicodeVersion": "8.0",
"digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c"
},
- {
- "name": "tone2",
- "unicode": "1F3FC",
+ "tone2": {
+ "category": "modifier",
+ "moji": "🏼",
+ "unicodeVersion": "8.0",
"digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f"
},
- {
- "name": "tone3",
- "unicode": "1F3FD",
+ "tone3": {
+ "category": "modifier",
+ "moji": "🏽",
+ "unicodeVersion": "8.0",
"digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8"
},
- {
- "name": "tone4",
- "unicode": "1F3FE",
+ "tone4": {
+ "category": "modifier",
+ "moji": "🏾",
+ "unicodeVersion": "8.0",
"digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3"
},
- {
- "name": "tone5",
- "unicode": "1F3FF",
+ "tone5": {
+ "category": "modifier",
+ "moji": "🏿",
+ "unicodeVersion": "8.0",
"digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81"
},
- {
- "name": "tongue",
- "unicode": "1F445",
+ "tongue": {
+ "category": "people",
+ "moji": "👅",
+ "unicodeVersion": "6.0",
"digest": "286e9d2583c371431d6fc979dd4ab48981676da26baada51a846657a3654c19b"
},
- {
- "name": "tools",
- "unicode": "1F6E0",
- "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
- },
- {
- "name": "hammer_and_wrench",
- "unicode": "1F6E0",
+ "tools": {
+ "category": "objects",
+ "moji": "🛠",
+ "unicodeVersion": "7.0",
"digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158"
},
- {
- "name": "top",
- "unicode": "1F51D",
+ "top": {
+ "category": "symbols",
+ "moji": "🔝",
+ "unicodeVersion": "6.0",
"digest": "c9a9f25b17db014e76b6be54aa07ef89bb18f8adb41b3199d180a559ff1d9ea5"
},
- {
- "name": "tophat",
- "unicode": "1F3A9",
+ "tophat": {
+ "category": "people",
+ "moji": "🎩",
+ "unicodeVersion": "6.0",
"digest": "43a45dfb5d6b57a63a0491f4e3ec780774c0301b53ed39a303a0bd803d16ed71"
},
- {
- "name": "track_next",
- "unicode": "23ED",
- "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
- },
- {
- "name": "next_track",
- "unicode": "23ED",
+ "track_next": {
+ "category": "symbols",
+ "moji": "⏭",
+ "unicodeVersion": "6.0",
"digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c"
},
- {
- "name": "track_previous",
- "unicode": "23EE",
- "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
- },
- {
- "name": "previous_track",
- "unicode": "23EE",
+ "track_previous": {
+ "category": "symbols",
+ "moji": "⏮",
+ "unicodeVersion": "6.0",
"digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87"
},
- {
- "name": "trackball",
- "unicode": "1F5B2",
+ "trackball": {
+ "category": "objects",
+ "moji": "🖲",
+ "unicodeVersion": "7.0",
"digest": "32a819a3129429f797ad434d0c40e263dc236808e34878c599ed2304b43702f5"
},
- {
- "name": "tractor",
- "unicode": "1F69C",
+ "tractor": {
+ "category": "travel",
+ "moji": "🚜",
+ "unicodeVersion": "6.0",
"digest": "5e4686290f1a4c9953ae208340b7d276f25b3b2197a43e52469aeb6450e93997"
},
- {
- "name": "traffic_light",
- "unicode": "1F6A5",
+ "traffic_light": {
+ "category": "travel",
+ "moji": "🚥",
+ "unicodeVersion": "6.0",
"digest": "d96aacade33d1ad3e0414f8a920513010f36eb7e5889774251c1d91148917ead"
},
- {
- "name": "train",
- "unicode": "1F68B",
+ "train": {
+ "category": "travel",
+ "moji": "🚋",
+ "unicodeVersion": "6.0",
"digest": "7423d17e131df7aadaa350b5d39dcbce3b28de331ff8b6703a3b2d0093963f4b"
},
- {
- "name": "train2",
- "unicode": "1F686",
+ "train2": {
+ "category": "travel",
+ "moji": "🚆",
+ "unicodeVersion": "6.0",
"digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122"
},
- {
- "name": "tram",
- "unicode": "1F68A",
+ "tram": {
+ "category": "travel",
+ "moji": "🚊",
+ "unicodeVersion": "6.0",
"digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100"
},
- {
- "name": "triangular_flag_on_post",
- "unicode": "1F6A9",
+ "triangular_flag_on_post": {
+ "category": "objects",
+ "moji": "🚩",
+ "unicodeVersion": "6.0",
"digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da"
},
- {
- "name": "triangular_ruler",
- "unicode": "1F4D0",
+ "triangular_ruler": {
+ "category": "objects",
+ "moji": "📐",
+ "unicodeVersion": "6.0",
"digest": "a0367dcf663ec934f1fc7c88bfaccc02b229a896f60930a66bb02241c933e501"
},
- {
- "name": "trident",
- "unicode": "1F531",
+ "trident": {
+ "category": "symbols",
+ "moji": "🔱",
+ "unicodeVersion": "6.0",
"digest": "ee45920845d3b35c2e45b934cf30ce97bfe2f24c5d72ef1ac6e0842e52b50fc1"
},
- {
- "name": "triumph",
- "unicode": "1F624",
+ "triumph": {
+ "category": "people",
+ "moji": "😤",
+ "unicodeVersion": "6.0",
"digest": "4aa44b8e1682c1269624a359f4b0bf613553683b883d947561ab169d7f85da0f"
},
- {
- "name": "trolleybus",
- "unicode": "1F68E",
+ "trolleybus": {
+ "category": "travel",
+ "moji": "🚎",
+ "unicodeVersion": "6.0",
"digest": "f610b4fd1123f06778a8e3bb8f738d5b0079aeb0b0926b6a63268c0dd0ee03ed"
},
- {
- "name": "trophy",
- "unicode": "1F3C6",
+ "trophy": {
+ "category": "activity",
+ "moji": "🏆",
+ "unicodeVersion": "6.0",
"digest": "50cfbedac18bf0fa5dec727643e15ec47f64068944b536e97518ee3be4f08006"
},
- {
- "name": "tropical_drink",
- "unicode": "1F379",
+ "tropical_drink": {
+ "category": "food",
+ "moji": "🍹",
+ "unicodeVersion": "6.0",
"digest": "54144fce60d650f426b1edf09e47c70b2762222398c1fe40231881f074603a69"
},
- {
- "name": "tropical_fish",
- "unicode": "1F420",
+ "tropical_fish": {
+ "category": "nature",
+ "moji": "🐠",
+ "unicodeVersion": "6.0",
"digest": "fd92100aaa9328da35e6090388824921b9726b474d1432a926d2cf9c45ad6528"
},
- {
- "name": "truck",
- "unicode": "1F69A",
+ "truck": {
+ "category": "travel",
+ "moji": "🚚",
+ "unicodeVersion": "6.0",
"digest": "0d1571e58e900abc453df0ff683fe7acb5906ecbdd52ab35b7101074359faf18"
},
- {
- "name": "trumpet",
- "unicode": "1F3BA",
+ "trumpet": {
+ "category": "activity",
+ "moji": "🎺",
+ "unicodeVersion": "6.0",
"digest": "cea3614c309f5573f328f4603120dbe930016a35f0dfa400b0d968fe9fff2d55"
},
- {
- "name": "tulip",
- "unicode": "1F337",
+ "tulip": {
+ "category": "nature",
+ "moji": "🌷",
+ "unicodeVersion": "6.0",
"digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086"
},
- {
- "name": "tumbler_glass",
- "unicode": "1F943",
- "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
- },
- {
- "name": "whisky",
- "unicode": "1F943",
+ "tumbler_glass": {
+ "category": "food",
+ "moji": "🥃",
+ "unicodeVersion": "9.0",
"digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
},
- {
- "name": "turkey",
- "unicode": "1F983",
+ "turkey": {
+ "category": "nature",
+ "moji": "🦃",
+ "unicodeVersion": "8.0",
"digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4"
},
- {
- "name": "turtle",
- "unicode": "1F422",
+ "turtle": {
+ "category": "nature",
+ "moji": "🐢",
+ "unicodeVersion": "6.0",
"digest": "588c35fb42c9502a908e9805517d4cc8c4ba4e74c9beed4035779fea1efe14f8"
},
- {
- "name": "tv",
- "unicode": "1F4FA",
+ "tv": {
+ "category": "objects",
+ "moji": "📺",
+ "unicodeVersion": "6.0",
"digest": "1279f3f3955a58dbbf74e248fc914b0bdba9c4c6b6a5176e9d12bf2750ecfeb4"
},
- {
- "name": "twisted_rightwards_arrows",
- "unicode": "1F500",
+ "twisted_rightwards_arrows": {
+ "category": "symbols",
+ "moji": "🔀",
+ "unicodeVersion": "6.0",
"digest": "fed07eebc2cf0d977ca0826bbd80defafbbcf118508444148f47b58949ebe27c"
},
- {
- "name": "two",
- "unicode": "0032-20E3",
+ "two": {
+ "category": "symbols",
+ "moji": "2️⃣",
+ "unicodeVersion": "3.0",
"digest": "b346f51f6523b02ebcbd753256804e2f9cc1574c96aa634362bf9401dac2c661"
},
- {
- "name": "two_hearts",
- "unicode": "1F495",
+ "two_hearts": {
+ "category": "symbols",
+ "moji": "💕",
+ "unicodeVersion": "6.0",
"digest": "6ded120a59aed790b441ec8fbbdea6f5cbfb4fa48e9e4b224cc29c9fde2d2e4c"
},
- {
- "name": "two_men_holding_hands",
- "unicode": "1F46C",
+ "two_men_holding_hands": {
+ "category": "people",
+ "moji": "👬",
+ "unicodeVersion": "6.0",
"digest": "bfcf9e20a67d00262cdf6e85f1acd545dda91f2e370d68bfd41ce02f232a2987"
},
- {
- "name": "two_women_holding_hands",
- "unicode": "1F46D",
+ "two_women_holding_hands": {
+ "category": "people",
+ "moji": "👭",
+ "unicodeVersion": "6.0",
"digest": "9d9d2b37a7f8e16fde1468dd8b5645003ea81ae4bf8bcf68471e2381845dd0dd"
},
- {
- "name": "u5272",
- "unicode": "1F239",
+ "u5272": {
+ "category": "symbols",
+ "moji": "🈹",
+ "unicodeVersion": "6.0",
"digest": "01e6cb8f74ea3c19fdade59c2d13d158b90dc6b4b293421b2014b7478bf20870"
},
- {
- "name": "u5408",
- "unicode": "1F234",
+ "u5408": {
+ "category": "symbols",
+ "moji": "🈴",
+ "unicodeVersion": "6.0",
"digest": "084cdbd5436670ea4dc22010e269c1ab7b0432897b8675301e69120374bcdd14"
},
- {
- "name": "u55b6",
- "unicode": "1F23A",
+ "u55b6": {
+ "category": "symbols",
+ "moji": "🈺",
+ "unicodeVersion": "6.0",
"digest": "c1017023d20d4aae78d59342dd3bfc5282716ea0601d9a8c2476335cbf7a2e12"
},
- {
- "name": "u6307",
- "unicode": "1F22F",
+ "u6307": {
+ "category": "symbols",
+ "moji": "🈯",
+ "unicodeVersion": "5.2",
"digest": "f459b092b974f459db1fb9cc13617a448b2e4f2b4dc46cc316d8c46af6e7d8bd"
},
- {
- "name": "u6708",
- "unicode": "1F237",
+ "u6708": {
+ "category": "symbols",
+ "moji": "🈷",
+ "unicodeVersion": "6.0",
"digest": "928815abf5b30f92efe5168de0c7e6cf8c17899a03e358ab42f42667e0a4a04c"
},
- {
- "name": "u6709",
- "unicode": "1F236",
+ "u6709": {
+ "category": "symbols",
+ "moji": "🈶",
+ "unicodeVersion": "6.0",
"digest": "f63a48ee06c892d24acec8b5634c021658d2ebde67a42d8faa86f27804a9f26d"
},
- {
- "name": "u6e80",
- "unicode": "1F235",
+ "u6e80": {
+ "category": "symbols",
+ "moji": "🈵",
+ "unicodeVersion": "6.0",
"digest": "489181d90a5e43068459530673a153e4af04fdad8514ec341ff7afbcfd366c3b"
},
- {
- "name": "u7121",
- "unicode": "1F21A",
+ "u7121": {
+ "category": "symbols",
+ "moji": "🈚",
+ "unicodeVersion": "5.2",
"digest": "9c50fd2ba14221affd2dcd3746322c2137dd75458493f4d385b544eb5bd8d6cd"
},
- {
- "name": "u7533",
- "unicode": "1F238",
+ "u7533": {
+ "category": "symbols",
+ "moji": "🈸",
+ "unicodeVersion": "6.0",
"digest": "2b05819b380a2ea47cc5fde8fcce3d53922fd223d6f5bd83d696d44175b69f18"
},
- {
- "name": "u7981",
- "unicode": "1F232",
+ "u7981": {
+ "category": "symbols",
+ "moji": "🈲",
+ "unicodeVersion": "6.0",
"digest": "adbe12601b22972003ddebcb0bd1532b979aa9c78bfdc147511854b5014eabc0"
},
- {
- "name": "u7a7a",
- "unicode": "1F233",
+ "u7a7a": {
+ "category": "symbols",
+ "moji": "🈳",
+ "unicodeVersion": "6.0",
"digest": "b9ee0ec7bb0b86c3eb73d4dbbb91848c427bf356ae30a263b9b44bd9bd784482"
},
- {
- "name": "umbrella",
- "unicode": "2614",
+ "umbrella": {
+ "category": "nature",
+ "moji": "☔",
+ "unicodeVersion": "4.0",
"digest": "0328a2f48b7df47905e2655460e524c0794ef12d3d7c32a049a10892d5662f77"
},
- {
- "name": "umbrella2",
- "unicode": "2602",
+ "umbrella2": {
+ "category": "nature",
+ "moji": "☂",
+ "unicodeVersion": "1.1",
"digest": "2f6a58110dc590480a822a3ffa2b5bc86f295e0c994a4a632837d25d4cf9fc58"
},
- {
- "name": "unamused",
- "unicode": "1F612",
+ "unamused": {
+ "category": "people",
+ "moji": "😒",
+ "unicodeVersion": "6.0",
"digest": "0d597088e3e7880918d0166e5c69243b18fe64afa31685c39bfdbc71494aa132"
},
- {
- "name": "underage",
- "unicode": "1F51E",
+ "underage": {
+ "category": "symbols",
+ "moji": "🔞",
+ "unicodeVersion": "6.0",
"digest": "b6b194614ca714ac2b1c2c17b75fe5922c7fdadb3d1157ba89ab2a5d03494a67"
},
- {
- "name": "unicorn",
- "unicode": "1F984",
- "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
- },
- {
- "name": "unicorn_face",
- "unicode": "1F984",
+ "unicorn": {
+ "category": "nature",
+ "moji": "🦄",
+ "unicodeVersion": "8.0",
"digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca"
},
- {
- "name": "unlock",
- "unicode": "1F513",
+ "unlock": {
+ "category": "objects",
+ "moji": "🔓",
+ "unicodeVersion": "6.0",
"digest": "9554ef3a6a315938b873e77970d9b0212e61f13c6cc36e4f17f87acc930a9a53"
},
- {
- "name": "up",
- "unicode": "1F199",
+ "up": {
+ "category": "symbols",
+ "moji": "🆙",
+ "unicodeVersion": "6.0",
"digest": "ff2554ccf08c7208b38794c5fa3d9a93a46ff191a49401195d8f740846121906"
},
- {
- "name": "upside_down",
- "unicode": "1F643",
- "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
- },
- {
- "name": "upside_down_face",
- "unicode": "1F643",
+ "upside_down": {
+ "category": "people",
+ "moji": "🙃",
+ "unicodeVersion": "8.0",
"digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1"
},
- {
- "name": "urn",
- "unicode": "26B1",
- "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
- },
- {
- "name": "funeral_urn",
- "unicode": "26B1",
+ "urn": {
+ "category": "objects",
+ "moji": "⚱",
+ "unicodeVersion": "4.1",
"digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6"
},
- {
- "name": "v",
- "unicode": "270C",
+ "v": {
+ "category": "people",
+ "moji": "✌",
+ "unicodeVersion": "1.1",
"digest": "9825bf440df289a8edf8ede494e8c778dc63c95f967f4d7bbea3245cf4f558ec"
},
- {
- "name": "v_tone1",
- "unicode": "270C-1F3FB",
+ "v_tone1": {
+ "category": "people",
+ "moji": "✌🏻",
+ "unicodeVersion": "8.0",
"digest": "76e358250d9ca519b60b8d7b6a32900700d784433dcc609e9442254a410f6e37"
},
- {
- "name": "v_tone2",
- "unicode": "270C-1F3FC",
+ "v_tone2": {
+ "category": "people",
+ "moji": "✌🏼",
+ "unicodeVersion": "8.0",
"digest": "4081b674be8416136022523fa9f29ec70a0f7e3aa05ca13152606609f3fd003c"
},
- {
- "name": "v_tone3",
- "unicode": "270C-1F3FD",
+ "v_tone3": {
+ "category": "people",
+ "moji": "✌🏽",
+ "unicodeVersion": "8.0",
"digest": "b6afb3a4c78384280610b953592d378241c75597a82aa6d16c86a993f8d8f3b0"
},
- {
- "name": "v_tone4",
- "unicode": "270C-1F3FE",
+ "v_tone4": {
+ "category": "people",
+ "moji": "✌🏾",
+ "unicodeVersion": "8.0",
"digest": "7ddc3cdd0138da2c8d7f6d8257ffdb8801496043e8a2395f93b0663447ac7fce"
},
- {
- "name": "v_tone5",
- "unicode": "270C-1F3FF",
+ "v_tone5": {
+ "category": "people",
+ "moji": "✌🏿",
+ "unicodeVersion": "8.0",
"digest": "a85dc5c589f0d1cf32f8bfa5c82e5c11c40b35439636914686a2f06f7359f539"
},
- {
- "name": "vertical_traffic_light",
- "unicode": "1F6A6",
+ "vertical_traffic_light": {
+ "category": "travel",
+ "moji": "🚦",
+ "unicodeVersion": "6.0",
"digest": "8cfd49a8f96b15a8313ef855f2e234ea3fa58332e68896dea34760740de9f020"
},
- {
- "name": "vhs",
- "unicode": "1F4FC",
+ "vhs": {
+ "category": "objects",
+ "moji": "📼",
+ "unicodeVersion": "6.0",
"digest": "3fb1acaf25805cf86f8d40ee2c17cf25da587b7ca93b931167ab43fce041eee8"
},
- {
- "name": "vibration_mode",
- "unicode": "1F4F3",
+ "vibration_mode": {
+ "category": "symbols",
+ "moji": "📳",
+ "unicodeVersion": "6.0",
"digest": "c9a8899222f46fe51dd8cee3e59f77c48268f0b7cfae2bcb34a791213acb1755"
},
- {
- "name": "video_camera",
- "unicode": "1F4F9",
+ "video_camera": {
+ "category": "objects",
+ "moji": "📹",
+ "unicodeVersion": "6.0",
"digest": "62e56f26c286a7964ef1021f0f23fcb4b38cdcfb5b5af569b472340c412c619a"
},
- {
- "name": "video_game",
- "unicode": "1F3AE",
+ "video_game": {
+ "category": "activity",
+ "moji": "🎮",
+ "unicodeVersion": "6.0",
"digest": "2787e302aa9e6fd7e9dc382c9bc7f5fbf244ef4940e08a4f9e80d33324f3032e"
},
- {
- "name": "violin",
- "unicode": "1F3BB",
+ "violin": {
+ "category": "activity",
+ "moji": "🎻",
+ "unicodeVersion": "6.0",
"digest": "1e69d531ce2b5d5bf1dd9470187dbbe76f479d14428834b6a9e2bf5296dc0ec9"
},
- {
- "name": "virgo",
- "unicode": "264D",
+ "virgo": {
+ "category": "symbols",
+ "moji": "♍",
+ "unicodeVersion": "1.1",
"digest": "0f75e9c228bc467fd0cec0f93f0e087c943bc5fb1d945fb0d4de53d07718388e"
},
- {
- "name": "volcano",
- "unicode": "1F30B",
+ "volcano": {
+ "category": "travel",
+ "moji": "🌋",
+ "unicodeVersion": "6.0",
"digest": "41c92ef88ca533df342a0ebe59d2b676873bfa944c3988495b8a96060a9b8e16"
},
- {
- "name": "volleyball",
- "unicode": "1F3D0",
+ "volleyball": {
+ "category": "activity",
+ "moji": "🏐",
+ "unicodeVersion": "8.0",
"digest": "774a83357f7aee890b4d4383236f0a90946dbd7c86aaabadc5753dcc9b4c9d69"
},
- {
- "name": "vs",
- "unicode": "1F19A",
+ "vs": {
+ "category": "symbols",
+ "moji": "🆚",
+ "unicodeVersion": "6.0",
"digest": "ac943e4c737459c2e1adbac8b71d3fdaebb704dbaf5713012e7a77beb09db1ef"
},
- {
- "name": "vulcan",
- "unicode": "1F596",
- "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
- },
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers",
- "unicode": "1F596",
+ "vulcan": {
+ "category": "people",
+ "moji": "🖖",
+ "unicodeVersion": "7.0",
"digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265"
},
- {
- "name": "vulcan_tone1",
- "unicode": "1F596-1F3FB",
- "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
- },
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1",
- "unicode": "1F596-1F3FB",
+ "vulcan_tone1": {
+ "category": "people",
+ "moji": "🖖🏻",
+ "unicodeVersion": "8.0",
"digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4"
},
- {
- "name": "vulcan_tone2",
- "unicode": "1F596-1F3FC",
- "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
- },
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2",
- "unicode": "1F596-1F3FC",
+ "vulcan_tone2": {
+ "category": "people",
+ "moji": "🖖🏼",
+ "unicodeVersion": "8.0",
"digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33"
},
- {
- "name": "vulcan_tone3",
- "unicode": "1F596-1F3FD",
+ "vulcan_tone3": {
+ "category": "people",
+ "moji": "🖖🏽",
+ "unicodeVersion": "8.0",
"digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
},
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3",
- "unicode": "1F596-1F3FD",
- "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a"
- },
- {
- "name": "vulcan_tone4",
- "unicode": "1F596-1F3FE",
- "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
- },
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4",
- "unicode": "1F596-1F3FE",
+ "vulcan_tone4": {
+ "category": "people",
+ "moji": "🖖🏾",
+ "unicodeVersion": "8.0",
"digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11"
},
- {
- "name": "vulcan_tone5",
- "unicode": "1F596-1F3FF",
+ "vulcan_tone5": {
+ "category": "people",
+ "moji": "🖖🏿",
+ "unicodeVersion": "8.0",
"digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
},
- {
- "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5",
- "unicode": "1F596-1F3FF",
- "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493"
- },
- {
- "name": "walking",
- "unicode": "1F6B6",
+ "walking": {
+ "category": "people",
+ "moji": "🚶",
+ "unicodeVersion": "6.0",
"digest": "ae77471fe1e8a734d11711cdb589f64347c35d6ee2fc10f6db16ac550c0557fa"
},
- {
- "name": "walking_tone1",
- "unicode": "1F6B6-1F3FB",
+ "walking_tone1": {
+ "category": "people",
+ "moji": "🚶🏻",
+ "unicodeVersion": "8.0",
"digest": "3de871c234e1340ccf95338df7babd94d175cfcb17a57b5a74d950e0a31f03b1"
},
- {
- "name": "walking_tone2",
- "unicode": "1F6B6-1F3FC",
+ "walking_tone2": {
+ "category": "people",
+ "moji": "🚶🏼",
+ "unicodeVersion": "8.0",
"digest": "620eb7bfb753a331a5822b02bdaf08d8dde7b573efd210287a3d3dfdd84a40b9"
},
- {
- "name": "walking_tone3",
- "unicode": "1F6B6-1F3FD",
+ "walking_tone3": {
+ "category": "people",
+ "moji": "🚶🏽",
+ "unicodeVersion": "8.0",
"digest": "ff39545acc2256006128f8c186433c28052b8c9aaec46fe06f25cff02c71f6b8"
},
- {
- "name": "walking_tone4",
- "unicode": "1F6B6-1F3FE",
+ "walking_tone4": {
+ "category": "people",
+ "moji": "🚶🏾",
+ "unicodeVersion": "8.0",
"digest": "a9499d142392977a9b9e54fb957952359e9bdffce7ec2f1e8320523d185fb066"
},
- {
- "name": "walking_tone5",
- "unicode": "1F6B6-1F3FF",
+ "walking_tone5": {
+ "category": "people",
+ "moji": "🚶🏿",
+ "unicodeVersion": "8.0",
"digest": "b47a4c48ce40298f842f454fc1abccae70f69725d73ee2c80e4018f4c4065d7d"
},
- {
- "name": "waning_crescent_moon",
- "unicode": "1F318",
+ "waning_crescent_moon": {
+ "category": "nature",
+ "moji": "🌘",
+ "unicodeVersion": "6.0",
"digest": "2ec7896eefcf821e0ea013556a17af59e997503662c07f080d0a84ab13ef4cf1"
},
- {
- "name": "waning_gibbous_moon",
- "unicode": "1F316",
+ "waning_gibbous_moon": {
+ "category": "nature",
+ "moji": "🌖",
+ "unicodeVersion": "6.0",
"digest": "ce2f5aca8fccdacaaf174d10da4e493e853e4608cc4d159aa3081d108a8b58d5"
},
- {
- "name": "warning",
- "unicode": "26A0",
+ "warning": {
+ "category": "symbols",
+ "moji": "⚠",
+ "unicodeVersion": "4.0",
"digest": "745f1d203958f42bf37ecb5909cd0819934e300308ba0ff20964c8c203092f90"
},
- {
- "name": "wastebasket",
- "unicode": "1F5D1",
+ "wastebasket": {
+ "category": "objects",
+ "moji": "🗑",
+ "unicodeVersion": "7.0",
"digest": "221a1b6d9975051038d9d97e18a16556cdf4254a6bca4c29bf1c51f306c79f2a"
},
- {
- "name": "watch",
- "unicode": "231A",
+ "watch": {
+ "category": "objects",
+ "moji": "⌚",
+ "unicodeVersion": "1.1",
"digest": "acc0c96751404a789b3085f10425cf34f942185215df459515d2439cde3efc6b"
},
- {
- "name": "water_buffalo",
- "unicode": "1F403",
+ "water_buffalo": {
+ "category": "nature",
+ "moji": "🐃",
+ "unicodeVersion": "6.0",
"digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1"
},
- {
- "name": "water_polo",
- "unicode": "1F93D",
+ "water_polo": {
+ "category": "activity",
+ "moji": "🤽",
+ "unicodeVersion": "9.0",
"digest": "fc77e1d2a84a9f4cf0cf19c1ea10cf137cf0940b9103a523121eda87677ad148"
},
- {
- "name": "water_polo_tone1",
- "unicode": "1F93D-1F3FB",
+ "water_polo_tone1": {
+ "category": "activity",
+ "moji": "🤽🏻",
+ "unicodeVersion": "9.0",
"digest": "3be28384edd29ada8109f07720d601a9d5866ed63e6234efe9ee1a194ed5d0c5"
},
- {
- "name": "water_polo_tone2",
- "unicode": "1F93D-1F3FC",
+ "water_polo_tone2": {
+ "category": "activity",
+ "moji": "🤽🏼",
+ "unicodeVersion": "9.0",
"digest": "afcd3f28c6719f869ca79a6fd1ccade2ea976ade844fbc1081fc72865bcb652f"
},
- {
- "name": "water_polo_tone3",
- "unicode": "1F93D-1F3FD",
+ "water_polo_tone3": {
+ "category": "activity",
+ "moji": "🤽🏽",
+ "unicodeVersion": "9.0",
"digest": "d19481c9b82d9413e99c2652e020fd763f2b54408dedaffec8dfe80973ded407"
},
- {
- "name": "water_polo_tone4",
- "unicode": "1F93D-1F3FE",
+ "water_polo_tone4": {
+ "category": "activity",
+ "moji": "🤽🏾",
+ "unicodeVersion": "9.0",
"digest": "375972d882b627e8d525e632e58b30346fc3e01858d7d08d62a9d3bf8132bbc7"
},
- {
- "name": "water_polo_tone5",
- "unicode": "1F93D-1F3FF",
+ "water_polo_tone5": {
+ "category": "activity",
+ "moji": "🤽🏿",
+ "unicodeVersion": "9.0",
"digest": "a8e1ced1c5382a8147a1d1801a133cada9a0e52e41de6272e56c3c1f426f6048"
},
- {
- "name": "watermelon",
- "unicode": "1F349",
+ "watermelon": {
+ "category": "food",
+ "moji": "🍉",
+ "unicodeVersion": "6.0",
"digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a"
},
- {
- "name": "wave",
- "unicode": "1F44B",
+ "wave": {
+ "category": "people",
+ "moji": "👋",
+ "unicodeVersion": "6.0",
"digest": "cddbd764d471604446cbaca91f77f6c4119d1cfc2c856732ca0eaac4593cb736"
},
- {
- "name": "wave_tone1",
- "unicode": "1F44B-1F3FB",
+ "wave_tone1": {
+ "category": "people",
+ "moji": "👋🏻",
+ "unicodeVersion": "8.0",
"digest": "cf40797437ddf68ec0275f337e6aac4bed81e28da7636d56c9f817ddf8e2b30a"
},
- {
- "name": "wave_tone2",
- "unicode": "1F44B-1F3FC",
+ "wave_tone2": {
+ "category": "people",
+ "moji": "👋🏼",
+ "unicodeVersion": "8.0",
"digest": "12c8a3e82c03ee35a734c642be482ba2d9d5948dacf91ec1fda243316dd4a0d0"
},
- {
- "name": "wave_tone3",
- "unicode": "1F44B-1F3FD",
+ "wave_tone3": {
+ "category": "people",
+ "moji": "👋🏽",
+ "unicodeVersion": "8.0",
"digest": "ebcaef43e21b475f76de811d4f4d1a67d9393973b57b03876e02164345a2ba4a"
},
- {
- "name": "wave_tone4",
- "unicode": "1F44B-1F3FE",
+ "wave_tone4": {
+ "category": "people",
+ "moji": "👋🏾",
+ "unicodeVersion": "8.0",
"digest": "7df7b70cf76766836ba146c3d91b6104930c384450cf2688426e60c1c06a1fc8"
},
- {
- "name": "wave_tone5",
- "unicode": "1F44B-1F3FF",
+ "wave_tone5": {
+ "category": "people",
+ "moji": "👋🏿",
+ "unicodeVersion": "8.0",
"digest": "8dfdba6aeff5d7dfd807467d431a137547726b34d021f1a5a0b74e155d270ea7"
},
- {
- "name": "wavy_dash",
- "unicode": "3030",
+ "wavy_dash": {
+ "category": "symbols",
+ "moji": "〰",
+ "unicodeVersion": "1.1",
"digest": "7b1968474f01d12fd09a1f2572282927138d9e9d6a3642de4bf68af80a8c3738"
},
- {
- "name": "waxing_crescent_moon",
- "unicode": "1F312",
+ "waxing_crescent_moon": {
+ "category": "nature",
+ "moji": "🌒",
+ "unicodeVersion": "6.0",
"digest": "852d7e55a19074d061fa3aa80d6b1e7e87a9280bdf44d94bbdbbe6d59178b1be"
},
- {
- "name": "waxing_gibbous_moon",
- "unicode": "1F314",
+ "waxing_gibbous_moon": {
+ "category": "nature",
+ "moji": "🌔",
+ "unicodeVersion": "6.0",
"digest": "a3a1c7cc72521a3f74929789a90e1c35d81ac86e21225c9f844d718d8940e3b3"
},
- {
- "name": "wc",
- "unicode": "1F6BE",
+ "wc": {
+ "category": "symbols",
+ "moji": "🚾",
+ "unicodeVersion": "6.0",
"digest": "4b95d54e0b53e4b705277917653503b32d6a143c2eaf6c547bc8e01c2dc23659"
},
- {
- "name": "weary",
- "unicode": "1F629",
+ "weary": {
+ "category": "people",
+ "moji": "😩",
+ "unicodeVersion": "6.0",
"digest": "3528f85540996cd5b562efe5421c495fc1bb414dc797bc20062783ae1b730847"
},
- {
- "name": "wedding",
- "unicode": "1F492",
+ "wedding": {
+ "category": "travel",
+ "moji": "💒",
+ "unicodeVersion": "6.0",
"digest": "980f3522cc4c19c3096e668032ea2cd19e7900cdc4b73bbb1c9b4c4d28dc78af"
},
- {
- "name": "whale",
- "unicode": "1F433",
+ "whale": {
+ "category": "nature",
+ "moji": "🐳",
+ "unicodeVersion": "6.0",
"digest": "6368fe4bc4a7f68aa2bd5386686a5f1b159feacbec16d59515f2b6e5d01adfbd"
},
- {
- "name": "whale2",
- "unicode": "1F40B",
+ "whale2": {
+ "category": "nature",
+ "moji": "🐋",
+ "unicodeVersion": "6.0",
"digest": "ccd3edf88167965f2abc18631ffb80e2532f728da35bc0c11144376685da18e8"
},
- {
- "name": "wheel_of_dharma",
- "unicode": "2638",
+ "wheel_of_dharma": {
+ "category": "symbols",
+ "moji": "☸",
+ "unicodeVersion": "1.1",
"digest": "4a0a13fcd507b9621686c8090bf340aa8770c064e0e3eb576fbae1229000d6da"
},
- {
- "name": "wheelchair",
- "unicode": "267F",
+ "wheelchair": {
+ "category": "symbols",
+ "moji": "♿",
+ "unicodeVersion": "4.1",
"digest": "f5250f2b4b5b4ffe6a6f77d30865c3f5d7173fc91aee547869589b2a96da91c8"
},
- {
- "name": "white_check_mark",
- "unicode": "2705",
+ "white_check_mark": {
+ "category": "symbols",
+ "moji": "✅",
+ "unicodeVersion": "6.0",
"digest": "45eb17bde6e503f22c8579d6e4d507ad6557a15f9eaad14aa716ec9ba1540876"
},
- {
- "name": "white_circle",
- "unicode": "26AA",
+ "white_circle": {
+ "category": "symbols",
+ "moji": "⚪",
+ "unicodeVersion": "4.1",
"digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c"
},
- {
- "name": "white_flower",
- "unicode": "1F4AE",
+ "white_flower": {
+ "category": "symbols",
+ "moji": "💮",
+ "unicodeVersion": "6.0",
"digest": "ace093b310eeefdecf4a4bdaf4fbcbb568457b0191ac80778a466ac5f3f4025a"
},
- {
- "name": "white_large_square",
- "unicode": "2B1C",
+ "white_large_square": {
+ "category": "symbols",
+ "moji": "⬜",
+ "unicodeVersion": "5.1",
"digest": "0db6957ee9ff7325b534b730fc05345a63d4ed9060f0f816807d0dcf004baa3e"
},
- {
- "name": "white_medium_small_square",
- "unicode": "25FD",
+ "white_medium_small_square": {
+ "category": "symbols",
+ "moji": "◽",
+ "unicodeVersion": "3.2",
"digest": "d79689981a7b38211c60a025a81e44fd39ac6ea4062e227cae3aab8f51572cd4"
},
- {
- "name": "white_medium_square",
- "unicode": "25FB",
+ "white_medium_square": {
+ "category": "symbols",
+ "moji": "◻",
+ "unicodeVersion": "3.2",
"digest": "6c4ce26d3f69667219f29ea18b04f3e79373024426275f25936e09a683e9a4fc"
},
- {
- "name": "white_small_square",
- "unicode": "25AB",
+ "white_small_square": {
+ "category": "symbols",
+ "moji": "▫",
+ "unicodeVersion": "1.1",
"digest": "ae0d35a6bbba4592b89b2f0f1f2d183efb2f93cf2a2136c0c195aab72f0bb1c8"
},
- {
- "name": "white_square_button",
- "unicode": "1F533",
+ "white_square_button": {
+ "category": "symbols",
+ "moji": "🔳",
+ "unicodeVersion": "6.0",
"digest": "797f3d9e44e88e940ffc118e52d0f709eec2ef14b13bdf873ad4b0c96cc0b042"
},
- {
- "name": "white_sun_cloud",
- "unicode": "1F325",
- "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
- },
- {
- "name": "white_sun_behind_cloud",
- "unicode": "1F325",
+ "white_sun_cloud": {
+ "category": "nature",
+ "moji": "🌥",
+ "unicodeVersion": "7.0",
"digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5"
},
- {
- "name": "white_sun_rain_cloud",
- "unicode": "1F326",
+ "white_sun_rain_cloud": {
+ "category": "nature",
+ "moji": "🌦",
+ "unicodeVersion": "7.0",
"digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
},
- {
- "name": "white_sun_behind_cloud_with_rain",
- "unicode": "1F326",
- "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5"
- },
- {
- "name": "white_sun_small_cloud",
- "unicode": "1F324",
- "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
- },
- {
- "name": "white_sun_with_small_cloud",
- "unicode": "1F324",
+ "white_sun_small_cloud": {
+ "category": "nature",
+ "moji": "🌤",
+ "unicodeVersion": "7.0",
"digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
},
- {
- "name": "wilted_rose",
- "unicode": "1F940",
+ "wilted_rose": {
+ "category": "nature",
+ "moji": "🥀",
+ "unicodeVersion": "9.0",
"digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
},
- {
- "name": "wilted_flower",
- "unicode": "1F940",
- "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
- },
- {
- "name": "wind_blowing_face",
- "unicode": "1F32C",
+ "wind_blowing_face": {
+ "category": "nature",
+ "moji": "🌬",
+ "unicodeVersion": "7.0",
"digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f"
},
- {
- "name": "wind_chime",
- "unicode": "1F390",
+ "wind_chime": {
+ "category": "objects",
+ "moji": "🎐",
+ "unicodeVersion": "6.0",
"digest": "1b1b212fbd74a9edc62aee7ffab9bcf91d3a9f69bffb2be4b7fd527914c14ced"
},
- {
- "name": "wine_glass",
- "unicode": "1F377",
+ "wine_glass": {
+ "category": "food",
+ "moji": "🍷",
+ "unicodeVersion": "6.0",
"digest": "d99107d6809386bc5e219aa58ee4930d27b7c3a6d2b10deb9f523df369f766d1"
},
- {
- "name": "wink",
- "unicode": "1F609",
+ "wink": {
+ "category": "people",
+ "moji": "😉",
+ "unicodeVersion": "6.0",
"digest": "56e29994a47335a901d0c98fa141d26faae8f647a860517bd3615fa980921885"
},
- {
- "name": "wolf",
- "unicode": "1F43A",
+ "wolf": {
+ "category": "nature",
+ "moji": "🐺",
+ "unicodeVersion": "6.0",
"digest": "4a983f5ec8ec0872fcde7890e17605b1229064e5e194b6fca1c4259068d1caed"
},
- {
- "name": "woman",
- "unicode": "1F469",
+ "woman": {
+ "category": "people",
+ "moji": "👩",
+ "unicodeVersion": "6.0",
"digest": "a06a22a48eeb3aeb885321358fe234e97797ed33be17f52d232ce2830cfbcd97"
},
- {
- "name": "woman_tone1",
- "unicode": "1F469-1F3FB",
+ "woman_tone1": {
+ "category": "people",
+ "moji": "👩🏻",
+ "unicodeVersion": "8.0",
"digest": "c2e4b135c1dac6a0b002569a6ccd9d098f6cb18481c68b5d9115e11241a0978d"
},
- {
- "name": "woman_tone2",
- "unicode": "1F469-1F3FC",
+ "woman_tone2": {
+ "category": "people",
+ "moji": "👩🏼",
+ "unicodeVersion": "8.0",
"digest": "4848e650051214a53c4cd9f6d3d94158f77f65ecb34f891789de34ee0a713006"
},
- {
- "name": "woman_tone3",
- "unicode": "1F469-1F3FD",
+ "woman_tone3": {
+ "category": "people",
+ "moji": "👩🏽",
+ "unicodeVersion": "8.0",
"digest": "b6f751ad47da019cdfb9d6d78f9610adb92120abf204c30df79a9150b57dbdee"
},
- {
- "name": "woman_tone4",
- "unicode": "1F469-1F3FE",
+ "woman_tone4": {
+ "category": "people",
+ "moji": "👩🏾",
+ "unicodeVersion": "8.0",
"digest": "fd27d3a669dc34313fbfe518df7dc2ded3ade5dde695f8d773afe87bf8a8b0d4"
},
- {
- "name": "woman_tone5",
- "unicode": "1F469-1F3FF",
+ "woman_tone5": {
+ "category": "people",
+ "moji": "👩🏿",
+ "unicodeVersion": "8.0",
"digest": "9ae9b14dfff40fa60a565d89479727feeba4fd6ffea9acb353a81b14aba751d4"
},
- {
- "name": "womans_clothes",
- "unicode": "1F45A",
+ "womans_clothes": {
+ "category": "people",
+ "moji": "👚",
+ "unicodeVersion": "6.0",
"digest": "d12a27810780fe5cd8118ed4587e0c4e70dbe9bcd014c6866fe6a8c9c7c55698"
},
- {
- "name": "womans_hat",
- "unicode": "1F452",
+ "womans_hat": {
+ "category": "people",
+ "moji": "👒",
+ "unicodeVersion": "6.0",
"digest": "52a0255b3483085bd125d39b74516ab6a81003964f44995c2fac821e7ff93086"
},
- {
- "name": "womens",
- "unicode": "1F6BA",
+ "womens": {
+ "category": "symbols",
+ "moji": "🚺",
+ "unicodeVersion": "6.0",
"digest": "7e38964006f8b28dfa2b3e9b2b16553bb50c18a63455f556b0bff35ee172137e"
},
- {
- "name": "worried",
- "unicode": "1F61F",
+ "worried": {
+ "category": "people",
+ "moji": "😟",
+ "unicodeVersion": "6.1",
"digest": "5a073985e1344bc34201ef94a491f7f2b946f5828c9fdbc57eeb2dcd87ac3a6b"
},
- {
- "name": "wrench",
- "unicode": "1F527",
+ "wrench": {
+ "category": "objects",
+ "moji": "🔧",
+ "unicodeVersion": "6.0",
"digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4"
},
- {
- "name": "wrestlers",
- "unicode": "1F93C",
- "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
- },
- {
- "name": "wrestling",
- "unicode": "1F93C",
+ "wrestlers": {
+ "category": "activity",
+ "moji": "🤼",
+ "unicodeVersion": "9.0",
"digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
},
- {
- "name": "wrestlers_tone1",
- "unicode": "1F93C-1F3FB",
+ "wrestlers_tone1": {
+ "category": "activity",
+ "moji": "🤼🏻",
+ "unicodeVersion": "9.0",
"digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
},
- {
- "name": "wrestling_tone1",
- "unicode": "1F93C-1F3FB",
- "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
- },
- {
- "name": "wrestlers_tone2",
- "unicode": "1F93C-1F3FC",
- "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
- },
- {
- "name": "wrestling_tone2",
- "unicode": "1F93C-1F3FC",
+ "wrestlers_tone2": {
+ "category": "activity",
+ "moji": "🤼🏼",
+ "unicodeVersion": "9.0",
"digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
},
- {
- "name": "wrestlers_tone3",
- "unicode": "1F93C-1F3FD",
+ "wrestlers_tone3": {
+ "category": "activity",
+ "moji": "🤼🏽",
+ "unicodeVersion": "9.0",
"digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
},
- {
- "name": "wrestling_tone3",
- "unicode": "1F93C-1F3FD",
- "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
- },
- {
- "name": "wrestlers_tone4",
- "unicode": "1F93C-1F3FE",
- "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
- },
- {
- "name": "wrestling_tone4",
- "unicode": "1F93C-1F3FE",
+ "wrestlers_tone4": {
+ "category": "activity",
+ "moji": "🤼🏾",
+ "unicodeVersion": "9.0",
"digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
},
- {
- "name": "wrestlers_tone5",
- "unicode": "1F93C-1F3FF",
+ "wrestlers_tone5": {
+ "category": "activity",
+ "moji": "🤼🏿",
+ "unicodeVersion": "9.0",
"digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
},
- {
- "name": "wrestling_tone5",
- "unicode": "1F93C-1F3FF",
- "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
- },
- {
- "name": "writing_hand",
- "unicode": "270D",
+ "writing_hand": {
+ "category": "people",
+ "moji": "✍",
+ "unicodeVersion": "1.1",
"digest": "110517ae4da5587e8b0662881658e27da4120bfacec54734fd6657831d4d782f"
},
- {
- "name": "writing_hand_tone1",
- "unicode": "270D-1F3FB",
+ "writing_hand_tone1": {
+ "category": "people",
+ "moji": "✍🏻",
+ "unicodeVersion": "8.0",
"digest": "2c7e2108e1990490b681343c1b01b4183d4f18fbdef792f113b2f87595e0dad0"
},
- {
- "name": "writing_hand_tone2",
- "unicode": "270D-1F3FC",
+ "writing_hand_tone2": {
+ "category": "people",
+ "moji": "✍🏼",
+ "unicodeVersion": "8.0",
"digest": "87ec8d44f472d301adbcbd50d8c852b609e46584057f59cc1527401db363c1bf"
},
- {
- "name": "writing_hand_tone3",
- "unicode": "270D-1F3FD",
+ "writing_hand_tone3": {
+ "category": "people",
+ "moji": "✍🏽",
+ "unicodeVersion": "8.0",
"digest": "4a48ddef91f7264e8fa9cca223554db22b3a2e3153e94b88d146644ea6dd661e"
},
- {
- "name": "writing_hand_tone4",
- "unicode": "270D-1F3FE",
+ "writing_hand_tone4": {
+ "category": "people",
+ "moji": "✍🏾",
+ "unicodeVersion": "8.0",
"digest": "e5254564a1f91e42ee59f359d8cd26f52abdc04dca8f3b37cb2f140cb7f71390"
},
- {
- "name": "writing_hand_tone5",
- "unicode": "270D-1F3FF",
+ "writing_hand_tone5": {
+ "category": "people",
+ "moji": "✍🏿",
+ "unicodeVersion": "8.0",
"digest": "61299bf86d83d323ca3e6052c535ae66c6f7b3d9866a37db0464223b8bc28523"
},
- {
- "name": "x",
- "unicode": "274C",
+ "x": {
+ "category": "symbols",
+ "moji": "❌",
+ "unicodeVersion": "6.0",
"digest": "3e5a7918e31ddefdf1ce73972365e2f0bfd2917d6a450c1a278c108349c9425d"
},
- {
- "name": "yellow_heart",
- "unicode": "1F49B",
+ "yellow_heart": {
+ "category": "symbols",
+ "moji": "💛",
+ "unicodeVersion": "6.0",
"digest": "a1098f2f04c29754cc9974324508386787d4d803b57cf691d42de414cb2679d6"
},
- {
- "name": "yen",
- "unicode": "1F4B4",
+ "yen": {
+ "category": "objects",
+ "moji": "💴",
+ "unicodeVersion": "6.0",
"digest": "944daaeb3f6369c807c0e63b106cee1360040f7800a70c0d942a992f25a55da7"
},
- {
- "name": "yin_yang",
- "unicode": "262F",
+ "yin_yang": {
+ "category": "symbols",
+ "moji": "☯",
+ "unicodeVersion": "1.1",
"digest": "5ee8d13dacf41306a09237bfcff6abeef110331b40eb7d6e80600628c1327545"
},
- {
- "name": "yum",
- "unicode": "1F60B",
+ "yum": {
+ "category": "people",
+ "moji": "😋",
+ "unicodeVersion": "6.0",
"digest": "31a89088c21bd7a74a3a26d731a907d1bc49436300a9f9c55248703cf7ef44c7"
},
- {
- "name": "zap",
- "unicode": "26A1",
+ "zap": {
+ "category": "nature",
+ "moji": "⚡",
+ "unicodeVersion": "4.0",
"digest": "9f8144ae6f866129aea41bbf694b0c858ef9352a139969e57cd8db73385f52c3"
},
- {
- "name": "zero",
- "unicode": "0030-20E3",
+ "zero": {
+ "category": "symbols",
+ "moji": "0️⃣",
+ "unicodeVersion": "3.0",
"digest": "1b27b5c904defadbdd28ace67a6be5c277ff043297db7cd9f672bbf84e37fa1a"
},
- {
- "name": "zipper_mouth",
- "unicode": "1F910",
- "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
- },
- {
- "name": "zipper_mouth_face",
- "unicode": "1F910",
+ "zipper_mouth": {
+ "category": "people",
+ "moji": "🤐",
+ "unicodeVersion": "8.0",
"digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43"
},
- {
- "name": "zzz",
- "unicode": "1F4A4",
+ "zzz": {
+ "category": "people",
+ "moji": "💤",
+ "unicodeVersion": "6.0",
"digest": "b3313d0c44a59fa9d4ce9f7eb4d07ff71dfc8bb01798154250f27cdcf3c693b5"
}
-] \ No newline at end of file
+} \ No newline at end of file
diff --git a/fixtures/emojis/emoji-unicode-version-map.json b/fixtures/emojis/emoji-unicode-version-map.json
new file mode 100644
index 00000000000..5164fe39426
--- /dev/null
+++ b/fixtures/emojis/emoji-unicode-version-map.json
@@ -0,0 +1,2377 @@
+{
+ "100": "6.0",
+ "1234": "6.0",
+ "grinning": "6.1",
+ "grin": "6.0",
+ "joy": "6.0",
+ "rofl": "9.0",
+ "rolling_on_the_floor_laughing": "9.0",
+ "smiley": "6.0",
+ "smile": "6.0",
+ "sweat_smile": "6.0",
+ "laughing": "6.0",
+ "satisfied": "6.0",
+ "wink": "6.0",
+ "blush": "6.0",
+ "yum": "6.0",
+ "sunglasses": "6.0",
+ "heart_eyes": "6.0",
+ "kissing_heart": "6.0",
+ "kissing": "6.1",
+ "kissing_smiling_eyes": "6.1",
+ "kissing_closed_eyes": "6.0",
+ "relaxed": "1.1",
+ "slight_smile": "7.0",
+ "slightly_smiling_face": "7.0",
+ "hugging": "8.0",
+ "hugging_face": "8.0",
+ "thinking": "8.0",
+ "thinking_face": "8.0",
+ "neutral_face": "6.0",
+ "expressionless": "6.1",
+ "no_mouth": "6.0",
+ "rolling_eyes": "8.0",
+ "face_with_rolling_eyes": "8.0",
+ "smirk": "6.0",
+ "persevere": "6.0",
+ "disappointed_relieved": "6.0",
+ "open_mouth": "6.1",
+ "zipper_mouth": "8.0",
+ "zipper_mouth_face": "8.0",
+ "hushed": "6.1",
+ "sleepy": "6.0",
+ "tired_face": "6.0",
+ "sleeping": "6.1",
+ "relieved": "6.0",
+ "nerd": "8.0",
+ "nerd_face": "8.0",
+ "stuck_out_tongue": "6.1",
+ "stuck_out_tongue_winking_eye": "6.0",
+ "stuck_out_tongue_closed_eyes": "6.0",
+ "drooling_face": "9.0",
+ "drool": "9.0",
+ "unamused": "6.0",
+ "sweat": "6.0",
+ "pensive": "6.0",
+ "confused": "6.1",
+ "upside_down": "8.0",
+ "upside_down_face": "8.0",
+ "money_mouth": "8.0",
+ "money_mouth_face": "8.0",
+ "astonished": "6.0",
+ "frowning2": "1.1",
+ "white_frowning_face": "1.1",
+ "slight_frown": "7.0",
+ "slightly_frowning_face": "7.0",
+ "confounded": "6.0",
+ "disappointed": "6.0",
+ "worried": "6.1",
+ "triumph": "6.0",
+ "cry": "6.0",
+ "sob": "6.0",
+ "frowning": "6.1",
+ "anguished": "6.1",
+ "fearful": "6.0",
+ "weary": "6.0",
+ "grimacing": "6.1",
+ "cold_sweat": "6.0",
+ "scream": "6.0",
+ "flushed": "6.0",
+ "dizzy_face": "6.0",
+ "rage": "6.0",
+ "angry": "6.0",
+ "innocent": "6.0",
+ "cowboy": "9.0",
+ "face_with_cowboy_hat": "9.0",
+ "clown": "9.0",
+ "clown_face": "9.0",
+ "lying_face": "9.0",
+ "liar": "9.0",
+ "mask": "6.0",
+ "thermometer_face": "8.0",
+ "face_with_thermometer": "8.0",
+ "head_bandage": "8.0",
+ "face_with_head_bandage": "8.0",
+ "nauseated_face": "9.0",
+ "sick": "9.0",
+ "sneezing_face": "9.0",
+ "sneeze": "9.0",
+ "smiling_imp": "6.0",
+ "imp": "6.0",
+ "japanese_ogre": "6.0",
+ "japanese_goblin": "6.0",
+ "skull": "6.0",
+ "skeleton": "6.0",
+ "skull_crossbones": "1.1",
+ "skull_and_crossbones": "1.1",
+ "ghost": "6.0",
+ "alien": "6.0",
+ "space_invader": "6.0",
+ "robot": "8.0",
+ "robot_face": "8.0",
+ "poop": "6.0",
+ "shit": "6.0",
+ "hankey": "6.0",
+ "poo": "6.0",
+ "smiley_cat": "6.0",
+ "smile_cat": "6.0",
+ "joy_cat": "6.0",
+ "heart_eyes_cat": "6.0",
+ "smirk_cat": "6.0",
+ "kissing_cat": "6.0",
+ "scream_cat": "6.0",
+ "crying_cat_face": "6.0",
+ "pouting_cat": "6.0",
+ "see_no_evil": "6.0",
+ "hear_no_evil": "6.0",
+ "speak_no_evil": "6.0",
+ "boy": "6.0",
+ "boy_tone1": "8.0",
+ "boy_tone2": "8.0",
+ "boy_tone3": "8.0",
+ "boy_tone4": "8.0",
+ "boy_tone5": "8.0",
+ "girl": "6.0",
+ "girl_tone1": "8.0",
+ "girl_tone2": "8.0",
+ "girl_tone3": "8.0",
+ "girl_tone4": "8.0",
+ "girl_tone5": "8.0",
+ "man": "6.0",
+ "man_tone1": "8.0",
+ "man_tone2": "8.0",
+ "man_tone3": "8.0",
+ "man_tone4": "8.0",
+ "man_tone5": "8.0",
+ "woman": "6.0",
+ "woman_tone1": "8.0",
+ "woman_tone2": "8.0",
+ "woman_tone3": "8.0",
+ "woman_tone4": "8.0",
+ "woman_tone5": "8.0",
+ "older_man": "6.0",
+ "older_man_tone1": "8.0",
+ "older_man_tone2": "8.0",
+ "older_man_tone3": "8.0",
+ "older_man_tone4": "8.0",
+ "older_man_tone5": "8.0",
+ "older_woman": "6.0",
+ "grandma": "6.0",
+ "older_woman_tone1": "8.0",
+ "grandma_tone1": "8.0",
+ "older_woman_tone2": "8.0",
+ "grandma_tone2": "8.0",
+ "older_woman_tone3": "8.0",
+ "grandma_tone3": "8.0",
+ "older_woman_tone4": "8.0",
+ "grandma_tone4": "8.0",
+ "older_woman_tone5": "8.0",
+ "grandma_tone5": "8.0",
+ "baby": "6.0",
+ "baby_tone1": "8.0",
+ "baby_tone2": "8.0",
+ "baby_tone3": "8.0",
+ "baby_tone4": "8.0",
+ "baby_tone5": "8.0",
+ "angel": "6.0",
+ "angel_tone1": "8.0",
+ "angel_tone2": "8.0",
+ "angel_tone3": "8.0",
+ "angel_tone4": "8.0",
+ "angel_tone5": "8.0",
+ "cop": "6.0",
+ "cop_tone1": "8.0",
+ "cop_tone2": "8.0",
+ "cop_tone3": "8.0",
+ "cop_tone4": "8.0",
+ "cop_tone5": "8.0",
+ "spy": "7.0",
+ "sleuth_or_spy": "7.0",
+ "spy_tone1": "8.0",
+ "sleuth_or_spy_tone1": "8.0",
+ "spy_tone2": "8.0",
+ "sleuth_or_spy_tone2": "8.0",
+ "spy_tone3": "8.0",
+ "sleuth_or_spy_tone3": "8.0",
+ "spy_tone4": "8.0",
+ "sleuth_or_spy_tone4": "8.0",
+ "spy_tone5": "8.0",
+ "sleuth_or_spy_tone5": "8.0",
+ "guardsman": "6.0",
+ "guardsman_tone1": "8.0",
+ "guardsman_tone2": "8.0",
+ "guardsman_tone3": "8.0",
+ "guardsman_tone4": "8.0",
+ "guardsman_tone5": "8.0",
+ "construction_worker": "6.0",
+ "construction_worker_tone1": "8.0",
+ "construction_worker_tone2": "8.0",
+ "construction_worker_tone3": "8.0",
+ "construction_worker_tone4": "8.0",
+ "construction_worker_tone5": "8.0",
+ "man_with_turban": "6.0",
+ "man_with_turban_tone1": "8.0",
+ "man_with_turban_tone2": "8.0",
+ "man_with_turban_tone3": "8.0",
+ "man_with_turban_tone4": "8.0",
+ "man_with_turban_tone5": "8.0",
+ "person_with_blond_hair": "6.0",
+ "person_with_blond_hair_tone1": "8.0",
+ "person_with_blond_hair_tone2": "8.0",
+ "person_with_blond_hair_tone3": "8.0",
+ "person_with_blond_hair_tone4": "8.0",
+ "person_with_blond_hair_tone5": "8.0",
+ "santa": "6.0",
+ "santa_tone1": "8.0",
+ "santa_tone2": "8.0",
+ "santa_tone3": "8.0",
+ "santa_tone4": "8.0",
+ "santa_tone5": "8.0",
+ "mrs_claus": "9.0",
+ "mother_christmas": "9.0",
+ "mrs_claus_tone1": "9.0",
+ "mother_christmas_tone1": "9.0",
+ "mrs_claus_tone2": "9.0",
+ "mother_christmas_tone2": "9.0",
+ "mrs_claus_tone3": "9.0",
+ "mother_christmas_tone3": "9.0",
+ "mrs_claus_tone4": "9.0",
+ "mother_christmas_tone4": "9.0",
+ "mrs_claus_tone5": "9.0",
+ "mother_christmas_tone5": "9.0",
+ "princess": "6.0",
+ "princess_tone1": "8.0",
+ "princess_tone2": "8.0",
+ "princess_tone3": "8.0",
+ "princess_tone4": "8.0",
+ "princess_tone5": "8.0",
+ "prince": "9.0",
+ "prince_tone1": "9.0",
+ "prince_tone2": "9.0",
+ "prince_tone3": "9.0",
+ "prince_tone4": "9.0",
+ "prince_tone5": "9.0",
+ "bride_with_veil": "6.0",
+ "bride_with_veil_tone1": "8.0",
+ "bride_with_veil_tone2": "8.0",
+ "bride_with_veil_tone3": "8.0",
+ "bride_with_veil_tone4": "8.0",
+ "bride_with_veil_tone5": "8.0",
+ "man_in_tuxedo": "9.0",
+ "man_in_tuxedo_tone1": "9.0",
+ "tuxedo_tone1": "9.0",
+ "man_in_tuxedo_tone2": "9.0",
+ "tuxedo_tone2": "9.0",
+ "man_in_tuxedo_tone3": "9.0",
+ "tuxedo_tone3": "9.0",
+ "man_in_tuxedo_tone4": "9.0",
+ "tuxedo_tone4": "9.0",
+ "man_in_tuxedo_tone5": "9.0",
+ "tuxedo_tone5": "9.0",
+ "pregnant_woman": "9.0",
+ "expecting_woman": "9.0",
+ "pregnant_woman_tone1": "9.0",
+ "expecting_woman_tone1": "9.0",
+ "pregnant_woman_tone2": "9.0",
+ "expecting_woman_tone2": "9.0",
+ "pregnant_woman_tone3": "9.0",
+ "expecting_woman_tone3": "9.0",
+ "pregnant_woman_tone4": "9.0",
+ "expecting_woman_tone4": "9.0",
+ "pregnant_woman_tone5": "9.0",
+ "expecting_woman_tone5": "9.0",
+ "man_with_gua_pi_mao": "6.0",
+ "man_with_gua_pi_mao_tone1": "8.0",
+ "man_with_gua_pi_mao_tone2": "8.0",
+ "man_with_gua_pi_mao_tone3": "8.0",
+ "man_with_gua_pi_mao_tone4": "8.0",
+ "man_with_gua_pi_mao_tone5": "8.0",
+ "person_frowning": "6.0",
+ "person_frowning_tone1": "8.0",
+ "person_frowning_tone2": "8.0",
+ "person_frowning_tone3": "8.0",
+ "person_frowning_tone4": "8.0",
+ "person_frowning_tone5": "8.0",
+ "person_with_pouting_face": "6.0",
+ "person_with_pouting_face_tone1": "8.0",
+ "person_with_pouting_face_tone2": "8.0",
+ "person_with_pouting_face_tone3": "8.0",
+ "person_with_pouting_face_tone4": "8.0",
+ "person_with_pouting_face_tone5": "8.0",
+ "no_good": "6.0",
+ "no_good_tone1": "8.0",
+ "no_good_tone2": "8.0",
+ "no_good_tone3": "8.0",
+ "no_good_tone4": "8.0",
+ "no_good_tone5": "8.0",
+ "ok_woman": "6.0",
+ "ok_woman_tone1": "8.0",
+ "ok_woman_tone2": "8.0",
+ "ok_woman_tone3": "8.0",
+ "ok_woman_tone4": "8.0",
+ "ok_woman_tone5": "8.0",
+ "information_desk_person": "6.0",
+ "information_desk_person_tone1": "8.0",
+ "information_desk_person_tone2": "8.0",
+ "information_desk_person_tone3": "8.0",
+ "information_desk_person_tone4": "8.0",
+ "information_desk_person_tone5": "8.0",
+ "raising_hand": "6.0",
+ "raising_hand_tone1": "8.0",
+ "raising_hand_tone2": "8.0",
+ "raising_hand_tone3": "8.0",
+ "raising_hand_tone4": "8.0",
+ "raising_hand_tone5": "8.0",
+ "bow": "6.0",
+ "bow_tone1": "8.0",
+ "bow_tone2": "8.0",
+ "bow_tone3": "8.0",
+ "bow_tone4": "8.0",
+ "bow_tone5": "8.0",
+ "face_palm": "9.0",
+ "facepalm": "9.0",
+ "face_palm_tone1": "9.0",
+ "facepalm_tone1": "9.0",
+ "face_palm_tone2": "9.0",
+ "facepalm_tone2": "9.0",
+ "face_palm_tone3": "9.0",
+ "facepalm_tone3": "9.0",
+ "face_palm_tone4": "9.0",
+ "facepalm_tone4": "9.0",
+ "face_palm_tone5": "9.0",
+ "facepalm_tone5": "9.0",
+ "shrug": "9.0",
+ "shrug_tone1": "9.0",
+ "shrug_tone2": "9.0",
+ "shrug_tone3": "9.0",
+ "shrug_tone4": "9.0",
+ "shrug_tone5": "9.0",
+ "massage": "6.0",
+ "massage_tone1": "8.0",
+ "massage_tone2": "8.0",
+ "massage_tone3": "8.0",
+ "massage_tone4": "8.0",
+ "massage_tone5": "8.0",
+ "haircut": "6.0",
+ "haircut_tone1": "8.0",
+ "haircut_tone2": "8.0",
+ "haircut_tone3": "8.0",
+ "haircut_tone4": "8.0",
+ "haircut_tone5": "8.0",
+ "walking": "6.0",
+ "walking_tone1": "8.0",
+ "walking_tone2": "8.0",
+ "walking_tone3": "8.0",
+ "walking_tone4": "8.0",
+ "walking_tone5": "8.0",
+ "runner": "6.0",
+ "runner_tone1": "8.0",
+ "runner_tone2": "8.0",
+ "runner_tone3": "8.0",
+ "runner_tone4": "8.0",
+ "runner_tone5": "8.0",
+ "dancer": "6.0",
+ "dancer_tone1": "8.0",
+ "dancer_tone2": "8.0",
+ "dancer_tone3": "8.0",
+ "dancer_tone4": "8.0",
+ "dancer_tone5": "8.0",
+ "man_dancing": "9.0",
+ "male_dancer": "9.0",
+ "man_dancing_tone1": "9.0",
+ "male_dancer_tone1": "9.0",
+ "man_dancing_tone2": "9.0",
+ "male_dancer_tone2": "9.0",
+ "man_dancing_tone3": "9.0",
+ "male_dancer_tone3": "9.0",
+ "man_dancing_tone4": "9.0",
+ "male_dancer_tone4": "9.0",
+ "man_dancing_tone5": "9.0",
+ "male_dancer_tone5": "9.0",
+ "dancers": "6.0",
+ "levitate": "7.0",
+ "man_in_business_suit_levitating": "7.0",
+ "speaking_head": "7.0",
+ "speaking_head_in_silhouette": "7.0",
+ "bust_in_silhouette": "6.0",
+ "busts_in_silhouette": "6.0",
+ "fencer": "9.0",
+ "fencing": "9.0",
+ "horse_racing": "6.0",
+ "horse_racing_tone1": "8.0",
+ "horse_racing_tone2": "8.0",
+ "horse_racing_tone3": "8.0",
+ "horse_racing_tone4": "8.0",
+ "horse_racing_tone5": "8.0",
+ "skier": "5.2",
+ "snowboarder": "6.0",
+ "golfer": "7.0",
+ "surfer": "6.0",
+ "surfer_tone1": "8.0",
+ "surfer_tone2": "8.0",
+ "surfer_tone3": "8.0",
+ "surfer_tone4": "8.0",
+ "surfer_tone5": "8.0",
+ "rowboat": "6.0",
+ "rowboat_tone1": "8.0",
+ "rowboat_tone2": "8.0",
+ "rowboat_tone3": "8.0",
+ "rowboat_tone4": "8.0",
+ "rowboat_tone5": "8.0",
+ "swimmer": "6.0",
+ "swimmer_tone1": "8.0",
+ "swimmer_tone2": "8.0",
+ "swimmer_tone3": "8.0",
+ "swimmer_tone4": "8.0",
+ "swimmer_tone5": "8.0",
+ "basketball_player": "5.2",
+ "person_with_ball": "5.2",
+ "basketball_player_tone1": "8.0",
+ "person_with_ball_tone1": "8.0",
+ "basketball_player_tone2": "8.0",
+ "person_with_ball_tone2": "8.0",
+ "basketball_player_tone3": "8.0",
+ "person_with_ball_tone3": "8.0",
+ "basketball_player_tone4": "8.0",
+ "person_with_ball_tone4": "8.0",
+ "basketball_player_tone5": "8.0",
+ "person_with_ball_tone5": "8.0",
+ "lifter": "7.0",
+ "weight_lifter": "7.0",
+ "lifter_tone1": "8.0",
+ "weight_lifter_tone1": "8.0",
+ "lifter_tone2": "8.0",
+ "weight_lifter_tone2": "8.0",
+ "lifter_tone3": "8.0",
+ "weight_lifter_tone3": "8.0",
+ "lifter_tone4": "8.0",
+ "weight_lifter_tone4": "8.0",
+ "lifter_tone5": "8.0",
+ "weight_lifter_tone5": "8.0",
+ "bicyclist": "6.0",
+ "bicyclist_tone1": "8.0",
+ "bicyclist_tone2": "8.0",
+ "bicyclist_tone3": "8.0",
+ "bicyclist_tone4": "8.0",
+ "bicyclist_tone5": "8.0",
+ "mountain_bicyclist": "6.0",
+ "mountain_bicyclist_tone1": "8.0",
+ "mountain_bicyclist_tone2": "8.0",
+ "mountain_bicyclist_tone3": "8.0",
+ "mountain_bicyclist_tone4": "8.0",
+ "mountain_bicyclist_tone5": "8.0",
+ "race_car": "7.0",
+ "racing_car": "7.0",
+ "motorcycle": "7.0",
+ "racing_motorcycle": "7.0",
+ "cartwheel": "9.0",
+ "person_doing_cartwheel": "9.0",
+ "cartwheel_tone1": "9.0",
+ "person_doing_cartwheel_tone1": "9.0",
+ "cartwheel_tone2": "9.0",
+ "person_doing_cartwheel_tone2": "9.0",
+ "cartwheel_tone3": "9.0",
+ "person_doing_cartwheel_tone3": "9.0",
+ "cartwheel_tone4": "9.0",
+ "person_doing_cartwheel_tone4": "9.0",
+ "cartwheel_tone5": "9.0",
+ "person_doing_cartwheel_tone5": "9.0",
+ "wrestlers": "9.0",
+ "wrestling": "9.0",
+ "wrestlers_tone1": "9.0",
+ "wrestling_tone1": "9.0",
+ "wrestlers_tone2": "9.0",
+ "wrestling_tone2": "9.0",
+ "wrestlers_tone3": "9.0",
+ "wrestling_tone3": "9.0",
+ "wrestlers_tone4": "9.0",
+ "wrestling_tone4": "9.0",
+ "wrestlers_tone5": "9.0",
+ "wrestling_tone5": "9.0",
+ "water_polo": "9.0",
+ "water_polo_tone1": "9.0",
+ "water_polo_tone2": "9.0",
+ "water_polo_tone3": "9.0",
+ "water_polo_tone4": "9.0",
+ "water_polo_tone5": "9.0",
+ "handball": "9.0",
+ "handball_tone1": "9.0",
+ "handball_tone2": "9.0",
+ "handball_tone3": "9.0",
+ "handball_tone4": "9.0",
+ "handball_tone5": "9.0",
+ "juggling": "9.0",
+ "juggler": "9.0",
+ "juggling_tone1": "9.0",
+ "juggler_tone1": "9.0",
+ "juggling_tone2": "9.0",
+ "juggler_tone2": "9.0",
+ "juggling_tone3": "9.0",
+ "juggler_tone3": "9.0",
+ "juggling_tone4": "9.0",
+ "juggler_tone4": "9.0",
+ "juggling_tone5": "9.0",
+ "juggler_tone5": "9.0",
+ "couple": "6.0",
+ "two_men_holding_hands": "6.0",
+ "two_women_holding_hands": "6.0",
+ "couplekiss": "6.0",
+ "kiss_mm": "6.0",
+ "couplekiss_mm": "6.0",
+ "kiss_ww": "6.0",
+ "couplekiss_ww": "6.0",
+ "couple_with_heart": "6.0",
+ "couple_mm": "6.0",
+ "couple_with_heart_mm": "6.0",
+ "couple_ww": "6.0",
+ "couple_with_heart_ww": "6.0",
+ "family": "6.0",
+ "family_mwg": "6.0",
+ "family_mwgb": "6.0",
+ "family_mwbb": "6.0",
+ "family_mwgg": "6.0",
+ "family_mmb": "6.0",
+ "family_mmg": "6.0",
+ "family_mmgb": "6.0",
+ "family_mmbb": "6.0",
+ "family_mmgg": "6.0",
+ "family_wwb": "6.0",
+ "family_wwg": "6.0",
+ "family_wwgb": "6.0",
+ "family_wwbb": "6.0",
+ "family_wwgg": "6.0",
+ "tone1": "8.0",
+ "tone2": "8.0",
+ "tone3": "8.0",
+ "tone4": "8.0",
+ "tone5": "8.0",
+ "muscle": "6.0",
+ "muscle_tone1": "8.0",
+ "muscle_tone2": "8.0",
+ "muscle_tone3": "8.0",
+ "muscle_tone4": "8.0",
+ "muscle_tone5": "8.0",
+ "selfie": "9.0",
+ "selfie_tone1": "9.0",
+ "selfie_tone2": "9.0",
+ "selfie_tone3": "9.0",
+ "selfie_tone4": "9.0",
+ "selfie_tone5": "9.0",
+ "point_left": "6.0",
+ "point_left_tone1": "8.0",
+ "point_left_tone2": "8.0",
+ "point_left_tone3": "8.0",
+ "point_left_tone4": "8.0",
+ "point_left_tone5": "8.0",
+ "point_right": "6.0",
+ "point_right_tone1": "8.0",
+ "point_right_tone2": "8.0",
+ "point_right_tone3": "8.0",
+ "point_right_tone4": "8.0",
+ "point_right_tone5": "8.0",
+ "point_up": "1.1",
+ "point_up_tone1": "8.0",
+ "point_up_tone2": "8.0",
+ "point_up_tone3": "8.0",
+ "point_up_tone4": "8.0",
+ "point_up_tone5": "8.0",
+ "point_up_2": "6.0",
+ "point_up_2_tone1": "8.0",
+ "point_up_2_tone2": "8.0",
+ "point_up_2_tone3": "8.0",
+ "point_up_2_tone4": "8.0",
+ "point_up_2_tone5": "8.0",
+ "middle_finger": "7.0",
+ "reversed_hand_with_middle_finger_extended": "7.0",
+ "middle_finger_tone1": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone1": "8.0",
+ "middle_finger_tone2": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone2": "8.0",
+ "middle_finger_tone3": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone3": "8.0",
+ "middle_finger_tone4": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone4": "8.0",
+ "middle_finger_tone5": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone5": "8.0",
+ "point_down": "6.0",
+ "point_down_tone1": "8.0",
+ "point_down_tone2": "8.0",
+ "point_down_tone3": "8.0",
+ "point_down_tone4": "8.0",
+ "point_down_tone5": "8.0",
+ "v": "1.1",
+ "v_tone1": "8.0",
+ "v_tone2": "8.0",
+ "v_tone3": "8.0",
+ "v_tone4": "8.0",
+ "v_tone5": "8.0",
+ "fingers_crossed": "9.0",
+ "hand_with_index_and_middle_finger_crossed": "9.0",
+ "fingers_crossed_tone1": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone1": "9.0",
+ "fingers_crossed_tone2": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone2": "9.0",
+ "fingers_crossed_tone3": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone3": "9.0",
+ "fingers_crossed_tone4": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone4": "9.0",
+ "fingers_crossed_tone5": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone5": "9.0",
+ "vulcan": "7.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers": "7.0",
+ "vulcan_tone1": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone1": "8.0",
+ "vulcan_tone2": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone2": "8.0",
+ "vulcan_tone3": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone3": "8.0",
+ "vulcan_tone4": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone4": "8.0",
+ "vulcan_tone5": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone5": "8.0",
+ "metal": "8.0",
+ "sign_of_the_horns": "8.0",
+ "metal_tone1": "8.0",
+ "sign_of_the_horns_tone1": "8.0",
+ "metal_tone2": "8.0",
+ "sign_of_the_horns_tone2": "8.0",
+ "metal_tone3": "8.0",
+ "sign_of_the_horns_tone3": "8.0",
+ "metal_tone4": "8.0",
+ "sign_of_the_horns_tone4": "8.0",
+ "metal_tone5": "8.0",
+ "sign_of_the_horns_tone5": "8.0",
+ "call_me": "9.0",
+ "call_me_hand": "9.0",
+ "call_me_tone1": "9.0",
+ "call_me_hand_tone1": "9.0",
+ "call_me_tone2": "9.0",
+ "call_me_hand_tone2": "9.0",
+ "call_me_tone3": "9.0",
+ "call_me_hand_tone3": "9.0",
+ "call_me_tone4": "9.0",
+ "call_me_hand_tone4": "9.0",
+ "call_me_tone5": "9.0",
+ "call_me_hand_tone5": "9.0",
+ "hand_splayed": "7.0",
+ "raised_hand_with_fingers_splayed": "7.0",
+ "hand_splayed_tone1": "8.0",
+ "raised_hand_with_fingers_splayed_tone1": "8.0",
+ "hand_splayed_tone2": "8.0",
+ "raised_hand_with_fingers_splayed_tone2": "8.0",
+ "hand_splayed_tone3": "8.0",
+ "raised_hand_with_fingers_splayed_tone3": "8.0",
+ "hand_splayed_tone4": "8.0",
+ "raised_hand_with_fingers_splayed_tone4": "8.0",
+ "hand_splayed_tone5": "8.0",
+ "raised_hand_with_fingers_splayed_tone5": "8.0",
+ "raised_hand": "6.0",
+ "raised_hand_tone1": "8.0",
+ "raised_hand_tone2": "8.0",
+ "raised_hand_tone3": "8.0",
+ "raised_hand_tone4": "8.0",
+ "raised_hand_tone5": "8.0",
+ "ok_hand": "6.0",
+ "ok_hand_tone1": "8.0",
+ "ok_hand_tone2": "8.0",
+ "ok_hand_tone3": "8.0",
+ "ok_hand_tone4": "8.0",
+ "ok_hand_tone5": "8.0",
+ "thumbsup": "6.0",
+ "+1": "6.0",
+ "thumbup": "6.0",
+ "thumbsup_tone1": "8.0",
+ "+1_tone1": "8.0",
+ "thumbup_tone1": "8.0",
+ "thumbsup_tone2": "8.0",
+ "+1_tone2": "8.0",
+ "thumbup_tone2": "8.0",
+ "thumbsup_tone3": "8.0",
+ "+1_tone3": "8.0",
+ "thumbup_tone3": "8.0",
+ "thumbsup_tone4": "8.0",
+ "+1_tone4": "8.0",
+ "thumbup_tone4": "8.0",
+ "thumbsup_tone5": "8.0",
+ "+1_tone5": "8.0",
+ "thumbup_tone5": "8.0",
+ "thumbsdown": "6.0",
+ "-1": "6.0",
+ "thumbdown": "6.0",
+ "thumbsdown_tone1": "8.0",
+ "-1_tone1": "8.0",
+ "thumbdown_tone1": "8.0",
+ "thumbsdown_tone2": "8.0",
+ "-1_tone2": "8.0",
+ "thumbdown_tone2": "8.0",
+ "thumbsdown_tone3": "8.0",
+ "-1_tone3": "8.0",
+ "thumbdown_tone3": "8.0",
+ "thumbsdown_tone4": "8.0",
+ "-1_tone4": "8.0",
+ "thumbdown_tone4": "8.0",
+ "thumbsdown_tone5": "8.0",
+ "-1_tone5": "8.0",
+ "thumbdown_tone5": "8.0",
+ "fist": "6.0",
+ "fist_tone1": "8.0",
+ "fist_tone2": "8.0",
+ "fist_tone3": "8.0",
+ "fist_tone4": "8.0",
+ "fist_tone5": "8.0",
+ "punch": "6.0",
+ "punch_tone1": "8.0",
+ "punch_tone2": "8.0",
+ "punch_tone3": "8.0",
+ "punch_tone4": "8.0",
+ "punch_tone5": "8.0",
+ "left_facing_fist": "9.0",
+ "left_fist": "9.0",
+ "left_facing_fist_tone1": "9.0",
+ "left_fist_tone1": "9.0",
+ "left_facing_fist_tone2": "9.0",
+ "left_fist_tone2": "9.0",
+ "left_facing_fist_tone3": "9.0",
+ "left_fist_tone3": "9.0",
+ "left_facing_fist_tone4": "9.0",
+ "left_fist_tone4": "9.0",
+ "left_facing_fist_tone5": "9.0",
+ "left_fist_tone5": "9.0",
+ "right_facing_fist": "9.0",
+ "right_fist": "9.0",
+ "right_facing_fist_tone1": "9.0",
+ "right_fist_tone1": "9.0",
+ "right_facing_fist_tone2": "9.0",
+ "right_fist_tone2": "9.0",
+ "right_facing_fist_tone3": "9.0",
+ "right_fist_tone3": "9.0",
+ "right_facing_fist_tone4": "9.0",
+ "right_fist_tone4": "9.0",
+ "right_facing_fist_tone5": "9.0",
+ "right_fist_tone5": "9.0",
+ "raised_back_of_hand": "9.0",
+ "back_of_hand": "9.0",
+ "raised_back_of_hand_tone1": "9.0",
+ "back_of_hand_tone1": "9.0",
+ "raised_back_of_hand_tone2": "9.0",
+ "back_of_hand_tone2": "9.0",
+ "raised_back_of_hand_tone3": "9.0",
+ "back_of_hand_tone3": "9.0",
+ "raised_back_of_hand_tone4": "9.0",
+ "back_of_hand_tone4": "9.0",
+ "raised_back_of_hand_tone5": "9.0",
+ "back_of_hand_tone5": "9.0",
+ "wave": "6.0",
+ "wave_tone1": "8.0",
+ "wave_tone2": "8.0",
+ "wave_tone3": "8.0",
+ "wave_tone4": "8.0",
+ "wave_tone5": "8.0",
+ "clap": "6.0",
+ "clap_tone1": "8.0",
+ "clap_tone2": "8.0",
+ "clap_tone3": "8.0",
+ "clap_tone4": "8.0",
+ "clap_tone5": "8.0",
+ "writing_hand": "1.1",
+ "writing_hand_tone1": "8.0",
+ "writing_hand_tone2": "8.0",
+ "writing_hand_tone3": "8.0",
+ "writing_hand_tone4": "8.0",
+ "writing_hand_tone5": "8.0",
+ "open_hands": "6.0",
+ "open_hands_tone1": "8.0",
+ "open_hands_tone2": "8.0",
+ "open_hands_tone3": "8.0",
+ "open_hands_tone4": "8.0",
+ "open_hands_tone5": "8.0",
+ "raised_hands": "6.0",
+ "raised_hands_tone1": "8.0",
+ "raised_hands_tone2": "8.0",
+ "raised_hands_tone3": "8.0",
+ "raised_hands_tone4": "8.0",
+ "raised_hands_tone5": "8.0",
+ "pray": "6.0",
+ "pray_tone1": "8.0",
+ "pray_tone2": "8.0",
+ "pray_tone3": "8.0",
+ "pray_tone4": "8.0",
+ "pray_tone5": "8.0",
+ "handshake": "9.0",
+ "shaking_hands": "9.0",
+ "handshake_tone1": "9.0",
+ "shaking_hands_tone1": "9.0",
+ "handshake_tone2": "9.0",
+ "shaking_hands_tone2": "9.0",
+ "handshake_tone3": "9.0",
+ "shaking_hands_tone3": "9.0",
+ "handshake_tone4": "9.0",
+ "shaking_hands_tone4": "9.0",
+ "handshake_tone5": "9.0",
+ "shaking_hands_tone5": "9.0",
+ "nail_care": "6.0",
+ "nail_care_tone1": "8.0",
+ "nail_care_tone2": "8.0",
+ "nail_care_tone3": "8.0",
+ "nail_care_tone4": "8.0",
+ "nail_care_tone5": "8.0",
+ "ear": "6.0",
+ "ear_tone1": "8.0",
+ "ear_tone2": "8.0",
+ "ear_tone3": "8.0",
+ "ear_tone4": "8.0",
+ "ear_tone5": "8.0",
+ "nose": "6.0",
+ "nose_tone1": "8.0",
+ "nose_tone2": "8.0",
+ "nose_tone3": "8.0",
+ "nose_tone4": "8.0",
+ "nose_tone5": "8.0",
+ "footprints": "6.0",
+ "eyes": "6.0",
+ "eye": "7.0",
+ "eye_in_speech_bubble": "7.0",
+ "tongue": "6.0",
+ "lips": "6.0",
+ "kiss": "6.0",
+ "cupid": "6.0",
+ "heart": "1.1",
+ "heartbeat": "6.0",
+ "broken_heart": "6.0",
+ "two_hearts": "6.0",
+ "sparkling_heart": "6.0",
+ "heartpulse": "6.0",
+ "blue_heart": "6.0",
+ "green_heart": "6.0",
+ "yellow_heart": "6.0",
+ "purple_heart": "6.0",
+ "black_heart": "9.0",
+ "gift_heart": "6.0",
+ "revolving_hearts": "6.0",
+ "heart_decoration": "6.0",
+ "heart_exclamation": "1.1",
+ "heavy_heart_exclamation_mark_ornament": "1.1",
+ "love_letter": "6.0",
+ "zzz": "6.0",
+ "anger": "6.0",
+ "bomb": "6.0",
+ "boom": "6.0",
+ "sweat_drops": "6.0",
+ "dash": "6.0",
+ "dizzy": "6.0",
+ "speech_balloon": "6.0",
+ "speech_left": "7.0",
+ "left_speech_bubble": "7.0",
+ "anger_right": "7.0",
+ "right_anger_bubble": "7.0",
+ "thought_balloon": "6.0",
+ "hole": "7.0",
+ "eyeglasses": "6.0",
+ "dark_sunglasses": "7.0",
+ "necktie": "6.0",
+ "shirt": "6.0",
+ "jeans": "6.0",
+ "dress": "6.0",
+ "kimono": "6.0",
+ "bikini": "6.0",
+ "womans_clothes": "6.0",
+ "purse": "6.0",
+ "handbag": "6.0",
+ "pouch": "6.0",
+ "shopping_bags": "7.0",
+ "school_satchel": "6.0",
+ "mans_shoe": "6.0",
+ "athletic_shoe": "6.0",
+ "high_heel": "6.0",
+ "sandal": "6.0",
+ "boot": "6.0",
+ "crown": "6.0",
+ "womans_hat": "6.0",
+ "tophat": "6.0",
+ "mortar_board": "6.0",
+ "helmet_with_cross": "5.2",
+ "helmet_with_white_cross": "5.2",
+ "prayer_beads": "8.0",
+ "lipstick": "6.0",
+ "ring": "6.0",
+ "gem": "6.0",
+ "monkey_face": "6.0",
+ "monkey": "6.0",
+ "gorilla": "9.0",
+ "dog": "6.0",
+ "dog2": "6.0",
+ "poodle": "6.0",
+ "wolf": "6.0",
+ "fox": "9.0",
+ "fox_face": "9.0",
+ "cat": "6.0",
+ "cat2": "6.0",
+ "lion_face": "8.0",
+ "lion": "8.0",
+ "tiger": "6.0",
+ "tiger2": "6.0",
+ "leopard": "6.0",
+ "horse": "6.0",
+ "racehorse": "6.0",
+ "deer": "9.0",
+ "unicorn": "8.0",
+ "unicorn_face": "8.0",
+ "cow": "6.0",
+ "ox": "6.0",
+ "water_buffalo": "6.0",
+ "cow2": "6.0",
+ "pig": "6.0",
+ "pig2": "6.0",
+ "boar": "6.0",
+ "pig_nose": "6.0",
+ "ram": "6.0",
+ "sheep": "6.0",
+ "goat": "6.0",
+ "dromedary_camel": "6.0",
+ "camel": "6.0",
+ "elephant": "6.0",
+ "rhino": "9.0",
+ "rhinoceros": "9.0",
+ "mouse": "6.0",
+ "mouse2": "6.0",
+ "rat": "6.0",
+ "hamster": "6.0",
+ "rabbit": "6.0",
+ "rabbit2": "6.0",
+ "chipmunk": "7.0",
+ "bat": "9.0",
+ "bear": "6.0",
+ "koala": "6.0",
+ "panda_face": "6.0",
+ "feet": "6.0",
+ "paw_prints": "6.0",
+ "turkey": "8.0",
+ "chicken": "6.0",
+ "rooster": "6.0",
+ "hatching_chick": "6.0",
+ "baby_chick": "6.0",
+ "hatched_chick": "6.0",
+ "bird": "6.0",
+ "penguin": "6.0",
+ "dove": "7.0",
+ "dove_of_peace": "7.0",
+ "eagle": "9.0",
+ "duck": "9.0",
+ "owl": "9.0",
+ "frog": "6.0",
+ "crocodile": "6.0",
+ "turtle": "6.0",
+ "lizard": "9.0",
+ "snake": "6.0",
+ "dragon_face": "6.0",
+ "dragon": "6.0",
+ "whale": "6.0",
+ "whale2": "6.0",
+ "dolphin": "6.0",
+ "fish": "6.0",
+ "tropical_fish": "6.0",
+ "blowfish": "6.0",
+ "shark": "9.0",
+ "octopus": "6.0",
+ "shell": "6.0",
+ "crab": "8.0",
+ "shrimp": "9.0",
+ "squid": "9.0",
+ "butterfly": "9.0",
+ "snail": "6.0",
+ "bug": "6.0",
+ "ant": "6.0",
+ "bee": "6.0",
+ "beetle": "6.0",
+ "spider": "7.0",
+ "spider_web": "7.0",
+ "scorpion": "8.0",
+ "bouquet": "6.0",
+ "cherry_blossom": "6.0",
+ "white_flower": "6.0",
+ "rosette": "7.0",
+ "rose": "6.0",
+ "wilted_rose": "9.0",
+ "wilted_flower": "9.0",
+ "hibiscus": "6.0",
+ "sunflower": "6.0",
+ "blossom": "6.0",
+ "tulip": "6.0",
+ "seedling": "6.0",
+ "evergreen_tree": "6.0",
+ "deciduous_tree": "6.0",
+ "palm_tree": "6.0",
+ "cactus": "6.0",
+ "ear_of_rice": "6.0",
+ "herb": "6.0",
+ "shamrock": "4.1",
+ "four_leaf_clover": "6.0",
+ "maple_leaf": "6.0",
+ "fallen_leaf": "6.0",
+ "leaves": "6.0",
+ "grapes": "6.0",
+ "melon": "6.0",
+ "watermelon": "6.0",
+ "tangerine": "6.0",
+ "lemon": "6.0",
+ "banana": "6.0",
+ "pineapple": "6.0",
+ "apple": "6.0",
+ "green_apple": "6.0",
+ "pear": "6.0",
+ "peach": "6.0",
+ "cherries": "6.0",
+ "strawberry": "6.0",
+ "kiwi": "9.0",
+ "kiwifruit": "9.0",
+ "tomato": "6.0",
+ "avocado": "9.0",
+ "eggplant": "6.0",
+ "potato": "9.0",
+ "carrot": "9.0",
+ "corn": "6.0",
+ "hot_pepper": "7.0",
+ "cucumber": "9.0",
+ "mushroom": "6.0",
+ "peanuts": "9.0",
+ "shelled_peanut": "9.0",
+ "chestnut": "6.0",
+ "bread": "6.0",
+ "croissant": "9.0",
+ "french_bread": "9.0",
+ "baguette_bread": "9.0",
+ "pancakes": "9.0",
+ "cheese": "8.0",
+ "cheese_wedge": "8.0",
+ "meat_on_bone": "6.0",
+ "poultry_leg": "6.0",
+ "bacon": "9.0",
+ "hamburger": "6.0",
+ "fries": "6.0",
+ "pizza": "6.0",
+ "hotdog": "8.0",
+ "hot_dog": "8.0",
+ "taco": "8.0",
+ "burrito": "8.0",
+ "stuffed_flatbread": "9.0",
+ "stuffed_pita": "9.0",
+ "egg": "9.0",
+ "cooking": "6.0",
+ "shallow_pan_of_food": "9.0",
+ "paella": "9.0",
+ "stew": "6.0",
+ "salad": "9.0",
+ "green_salad": "9.0",
+ "popcorn": "8.0",
+ "bento": "6.0",
+ "rice_cracker": "6.0",
+ "rice_ball": "6.0",
+ "rice": "6.0",
+ "curry": "6.0",
+ "ramen": "6.0",
+ "spaghetti": "6.0",
+ "sweet_potato": "6.0",
+ "oden": "6.0",
+ "sushi": "6.0",
+ "fried_shrimp": "6.0",
+ "fish_cake": "6.0",
+ "dango": "6.0",
+ "icecream": "6.0",
+ "shaved_ice": "6.0",
+ "ice_cream": "6.0",
+ "doughnut": "6.0",
+ "cookie": "6.0",
+ "birthday": "6.0",
+ "cake": "6.0",
+ "chocolate_bar": "6.0",
+ "candy": "6.0",
+ "lollipop": "6.0",
+ "custard": "6.0",
+ "pudding": "6.0",
+ "flan": "6.0",
+ "honey_pot": "6.0",
+ "baby_bottle": "6.0",
+ "milk": "9.0",
+ "glass_of_milk": "9.0",
+ "coffee": "4.0",
+ "tea": "6.0",
+ "sake": "6.0",
+ "champagne": "8.0",
+ "bottle_with_popping_cork": "8.0",
+ "wine_glass": "6.0",
+ "cocktail": "6.0",
+ "tropical_drink": "6.0",
+ "beer": "6.0",
+ "beers": "6.0",
+ "champagne_glass": "9.0",
+ "clinking_glass": "9.0",
+ "tumbler_glass": "9.0",
+ "whisky": "9.0",
+ "fork_knife_plate": "7.0",
+ "fork_and_knife_with_plate": "7.0",
+ "fork_and_knife": "6.0",
+ "spoon": "9.0",
+ "knife": "6.0",
+ "amphora": "8.0",
+ "earth_africa": "6.0",
+ "earth_americas": "6.0",
+ "earth_asia": "6.0",
+ "globe_with_meridians": "6.0",
+ "map": "7.0",
+ "world_map": "7.0",
+ "japan": "6.0",
+ "mountain_snow": "7.0",
+ "snow_capped_mountain": "7.0",
+ "mountain": "5.2",
+ "volcano": "6.0",
+ "mount_fuji": "6.0",
+ "camping": "7.0",
+ "beach": "7.0",
+ "beach_with_umbrella": "7.0",
+ "desert": "7.0",
+ "island": "7.0",
+ "desert_island": "7.0",
+ "park": "7.0",
+ "national_park": "7.0",
+ "stadium": "7.0",
+ "classical_building": "7.0",
+ "construction_site": "7.0",
+ "building_construction": "7.0",
+ "homes": "7.0",
+ "house_buildings": "7.0",
+ "cityscape": "7.0",
+ "house_abandoned": "7.0",
+ "derelict_house_building": "7.0",
+ "house": "6.0",
+ "house_with_garden": "6.0",
+ "office": "6.0",
+ "post_office": "6.0",
+ "european_post_office": "6.0",
+ "hospital": "6.0",
+ "bank": "6.0",
+ "hotel": "6.0",
+ "love_hotel": "6.0",
+ "convenience_store": "6.0",
+ "school": "6.0",
+ "department_store": "6.0",
+ "factory": "6.0",
+ "japanese_castle": "6.0",
+ "european_castle": "6.0",
+ "wedding": "6.0",
+ "tokyo_tower": "6.0",
+ "statue_of_liberty": "6.0",
+ "church": "5.2",
+ "mosque": "8.0",
+ "synagogue": "8.0",
+ "shinto_shrine": "5.2",
+ "kaaba": "8.0",
+ "fountain": "5.2",
+ "tent": "5.2",
+ "foggy": "6.0",
+ "night_with_stars": "6.0",
+ "sunrise_over_mountains": "6.0",
+ "sunrise": "6.0",
+ "city_dusk": "6.0",
+ "city_sunset": "6.0",
+ "city_sunrise": "6.0",
+ "bridge_at_night": "6.0",
+ "hotsprings": "1.1",
+ "milky_way": "6.0",
+ "carousel_horse": "6.0",
+ "ferris_wheel": "6.0",
+ "roller_coaster": "6.0",
+ "barber": "6.0",
+ "circus_tent": "6.0",
+ "performing_arts": "6.0",
+ "frame_photo": "7.0",
+ "frame_with_picture": "7.0",
+ "art": "6.0",
+ "slot_machine": "6.0",
+ "steam_locomotive": "6.0",
+ "railway_car": "6.0",
+ "bullettrain_side": "6.0",
+ "bullettrain_front": "6.0",
+ "train2": "6.0",
+ "metro": "6.0",
+ "light_rail": "6.0",
+ "station": "6.0",
+ "tram": "6.0",
+ "monorail": "6.0",
+ "mountain_railway": "6.0",
+ "train": "6.0",
+ "bus": "6.0",
+ "oncoming_bus": "6.0",
+ "trolleybus": "6.0",
+ "minibus": "6.0",
+ "ambulance": "6.0",
+ "fire_engine": "6.0",
+ "police_car": "6.0",
+ "oncoming_police_car": "6.0",
+ "taxi": "6.0",
+ "oncoming_taxi": "6.0",
+ "red_car": "6.0",
+ "oncoming_automobile": "6.0",
+ "blue_car": "6.0",
+ "truck": "6.0",
+ "articulated_lorry": "6.0",
+ "tractor": "6.0",
+ "bike": "6.0",
+ "scooter": "9.0",
+ "motor_scooter": "9.0",
+ "motorbike": "9.0",
+ "busstop": "6.0",
+ "motorway": "7.0",
+ "railway_track": "7.0",
+ "railroad_track": "7.0",
+ "fuelpump": "5.2",
+ "rotating_light": "6.0",
+ "traffic_light": "6.0",
+ "vertical_traffic_light": "6.0",
+ "construction": "6.0",
+ "octagonal_sign": "9.0",
+ "stop_sign": "9.0",
+ "anchor": "4.1",
+ "sailboat": "5.2",
+ "canoe": "9.0",
+ "kayak": "9.0",
+ "speedboat": "6.0",
+ "cruise_ship": "7.0",
+ "passenger_ship": "7.0",
+ "ferry": "5.2",
+ "motorboat": "7.0",
+ "ship": "6.0",
+ "airplane": "1.1",
+ "airplane_small": "7.0",
+ "small_airplane": "7.0",
+ "airplane_departure": "7.0",
+ "airplane_arriving": "7.0",
+ "seat": "6.0",
+ "helicopter": "6.0",
+ "suspension_railway": "6.0",
+ "mountain_cableway": "6.0",
+ "aerial_tramway": "6.0",
+ "rocket": "6.0",
+ "satellite_orbital": "7.0",
+ "bellhop": "7.0",
+ "bellhop_bell": "7.0",
+ "door": "6.0",
+ "sleeping_accommodation": "7.0",
+ "bed": "7.0",
+ "couch": "7.0",
+ "couch_and_lamp": "7.0",
+ "toilet": "6.0",
+ "shower": "6.0",
+ "bath": "6.0",
+ "bath_tone1": "8.0",
+ "bath_tone2": "8.0",
+ "bath_tone3": "8.0",
+ "bath_tone4": "8.0",
+ "bath_tone5": "8.0",
+ "bathtub": "6.0",
+ "hourglass": "1.1",
+ "hourglass_flowing_sand": "6.0",
+ "watch": "1.1",
+ "alarm_clock": "6.0",
+ "stopwatch": "6.0",
+ "timer": "6.0",
+ "timer_clock": "6.0",
+ "clock": "7.0",
+ "mantlepiece_clock": "7.0",
+ "clock12": "6.0",
+ "clock1230": "6.0",
+ "clock1": "6.0",
+ "clock130": "6.0",
+ "clock2": "6.0",
+ "clock230": "6.0",
+ "clock3": "6.0",
+ "clock330": "6.0",
+ "clock4": "6.0",
+ "clock430": "6.0",
+ "clock5": "6.0",
+ "clock530": "6.0",
+ "clock6": "6.0",
+ "clock630": "6.0",
+ "clock7": "6.0",
+ "clock730": "6.0",
+ "clock8": "6.0",
+ "clock830": "6.0",
+ "clock9": "6.0",
+ "clock930": "6.0",
+ "clock10": "6.0",
+ "clock1030": "6.0",
+ "clock11": "6.0",
+ "clock1130": "6.0",
+ "new_moon": "6.0",
+ "waxing_crescent_moon": "6.0",
+ "first_quarter_moon": "6.0",
+ "waxing_gibbous_moon": "6.0",
+ "full_moon": "6.0",
+ "waning_gibbous_moon": "6.0",
+ "last_quarter_moon": "6.0",
+ "waning_crescent_moon": "6.0",
+ "crescent_moon": "6.0",
+ "new_moon_with_face": "6.0",
+ "first_quarter_moon_with_face": "6.0",
+ "last_quarter_moon_with_face": "6.0",
+ "thermometer": "7.0",
+ "sunny": "1.1",
+ "full_moon_with_face": "6.0",
+ "sun_with_face": "6.0",
+ "star": "5.1",
+ "star2": "6.0",
+ "stars": "6.0",
+ "cloud": "1.1",
+ "partly_sunny": "5.2",
+ "thunder_cloud_rain": "5.2",
+ "thunder_cloud_and_rain": "5.2",
+ "white_sun_small_cloud": "7.0",
+ "white_sun_with_small_cloud": "7.0",
+ "white_sun_cloud": "7.0",
+ "white_sun_behind_cloud": "7.0",
+ "white_sun_rain_cloud": "7.0",
+ "white_sun_behind_cloud_with_rain": "7.0",
+ "cloud_rain": "7.0",
+ "cloud_with_rain": "7.0",
+ "cloud_snow": "7.0",
+ "cloud_with_snow": "7.0",
+ "cloud_lightning": "7.0",
+ "cloud_with_lightning": "7.0",
+ "cloud_tornado": "7.0",
+ "cloud_with_tornado": "7.0",
+ "fog": "7.0",
+ "wind_blowing_face": "7.0",
+ "cyclone": "6.0",
+ "rainbow": "6.0",
+ "closed_umbrella": "6.0",
+ "umbrella2": "1.1",
+ "umbrella": "4.0",
+ "beach_umbrella": "5.2",
+ "umbrella_on_ground": "5.2",
+ "zap": "4.0",
+ "snowflake": "1.1",
+ "snowman2": "1.1",
+ "snowman": "5.2",
+ "comet": "1.1",
+ "fire": "6.0",
+ "flame": "6.0",
+ "droplet": "6.0",
+ "ocean": "6.0",
+ "jack_o_lantern": "6.0",
+ "christmas_tree": "6.0",
+ "fireworks": "6.0",
+ "sparkler": "6.0",
+ "sparkles": "6.0",
+ "balloon": "6.0",
+ "tada": "6.0",
+ "confetti_ball": "6.0",
+ "tanabata_tree": "6.0",
+ "bamboo": "6.0",
+ "dolls": "6.0",
+ "flags": "6.0",
+ "wind_chime": "6.0",
+ "rice_scene": "6.0",
+ "ribbon": "6.0",
+ "gift": "6.0",
+ "reminder_ribbon": "7.0",
+ "tickets": "7.0",
+ "admission_tickets": "7.0",
+ "ticket": "6.0",
+ "military_medal": "7.0",
+ "trophy": "6.0",
+ "medal": "7.0",
+ "sports_medal": "7.0",
+ "first_place": "9.0",
+ "first_place_medal": "9.0",
+ "second_place": "9.0",
+ "second_place_medal": "9.0",
+ "third_place": "9.0",
+ "third_place_medal": "9.0",
+ "soccer": "5.2",
+ "baseball": "5.2",
+ "basketball": "6.0",
+ "volleyball": "8.0",
+ "football": "6.0",
+ "rugby_football": "6.0",
+ "tennis": "6.0",
+ "8ball": "6.0",
+ "bowling": "6.0",
+ "cricket": "8.0",
+ "cricket_bat_ball": "8.0",
+ "field_hockey": "8.0",
+ "hockey": "8.0",
+ "ping_pong": "8.0",
+ "table_tennis": "8.0",
+ "badminton": "8.0",
+ "boxing_glove": "9.0",
+ "boxing_gloves": "9.0",
+ "martial_arts_uniform": "9.0",
+ "karate_uniform": "9.0",
+ "goal": "9.0",
+ "goal_net": "9.0",
+ "dart": "6.0",
+ "golf": "5.2",
+ "ice_skate": "5.2",
+ "fishing_pole_and_fish": "6.0",
+ "running_shirt_with_sash": "6.0",
+ "ski": "6.0",
+ "video_game": "6.0",
+ "joystick": "7.0",
+ "game_die": "6.0",
+ "spades": "1.1",
+ "hearts": "1.1",
+ "diamonds": "1.1",
+ "clubs": "1.1",
+ "black_joker": "6.0",
+ "mahjong": "5.1",
+ "flower_playing_cards": "6.0",
+ "mute": "6.0",
+ "speaker": "6.0",
+ "sound": "6.0",
+ "loud_sound": "6.0",
+ "loudspeaker": "6.0",
+ "mega": "6.0",
+ "postal_horn": "6.0",
+ "bell": "6.0",
+ "no_bell": "6.0",
+ "musical_score": "6.0",
+ "musical_note": "6.0",
+ "notes": "6.0",
+ "microphone2": "7.0",
+ "studio_microphone": "7.0",
+ "level_slider": "7.0",
+ "control_knobs": "7.0",
+ "microphone": "6.0",
+ "headphones": "6.0",
+ "radio": "6.0",
+ "saxophone": "6.0",
+ "guitar": "6.0",
+ "musical_keyboard": "6.0",
+ "trumpet": "6.0",
+ "violin": "6.0",
+ "drum": "9.0",
+ "drum_with_drumsticks": "9.0",
+ "iphone": "6.0",
+ "calling": "6.0",
+ "telephone": "1.1",
+ "telephone_receiver": "6.0",
+ "pager": "6.0",
+ "fax": "6.0",
+ "battery": "6.0",
+ "electric_plug": "6.0",
+ "computer": "6.0",
+ "desktop": "7.0",
+ "desktop_computer": "7.0",
+ "printer": "7.0",
+ "keyboard": "1.1",
+ "mouse_three_button": "7.0",
+ "three_button_mouse": "7.0",
+ "trackball": "7.0",
+ "minidisc": "6.0",
+ "floppy_disk": "6.0",
+ "cd": "6.0",
+ "dvd": "6.0",
+ "movie_camera": "6.0",
+ "film_frames": "7.0",
+ "projector": "7.0",
+ "film_projector": "7.0",
+ "clapper": "6.0",
+ "tv": "6.0",
+ "camera": "6.0",
+ "camera_with_flash": "7.0",
+ "video_camera": "6.0",
+ "vhs": "6.0",
+ "mag": "6.0",
+ "mag_right": "6.0",
+ "microscope": "6.0",
+ "telescope": "6.0",
+ "satellite": "6.0",
+ "candle": "7.0",
+ "bulb": "6.0",
+ "flashlight": "6.0",
+ "izakaya_lantern": "6.0",
+ "notebook_with_decorative_cover": "6.0",
+ "closed_book": "6.0",
+ "book": "6.0",
+ "green_book": "6.0",
+ "blue_book": "6.0",
+ "orange_book": "6.0",
+ "books": "6.0",
+ "notebook": "6.0",
+ "ledger": "6.0",
+ "page_with_curl": "6.0",
+ "scroll": "6.0",
+ "page_facing_up": "6.0",
+ "newspaper": "6.0",
+ "newspaper2": "7.0",
+ "rolled_up_newspaper": "7.0",
+ "bookmark_tabs": "6.0",
+ "bookmark": "6.0",
+ "label": "7.0",
+ "moneybag": "6.0",
+ "yen": "6.0",
+ "dollar": "6.0",
+ "euro": "6.0",
+ "pound": "6.0",
+ "money_with_wings": "6.0",
+ "credit_card": "6.0",
+ "chart": "6.0",
+ "currency_exchange": "6.0",
+ "heavy_dollar_sign": "6.0",
+ "envelope": "1.1",
+ "e-mail": "6.0",
+ "email": "6.0",
+ "incoming_envelope": "6.0",
+ "envelope_with_arrow": "6.0",
+ "outbox_tray": "6.0",
+ "inbox_tray": "6.0",
+ "package": "6.0",
+ "mailbox": "6.0",
+ "mailbox_closed": "6.0",
+ "mailbox_with_mail": "6.0",
+ "mailbox_with_no_mail": "6.0",
+ "postbox": "6.0",
+ "ballot_box": "7.0",
+ "ballot_box_with_ballot": "7.0",
+ "pencil2": "1.1",
+ "black_nib": "1.1",
+ "pen_fountain": "7.0",
+ "lower_left_fountain_pen": "7.0",
+ "pen_ballpoint": "7.0",
+ "lower_left_ballpoint_pen": "7.0",
+ "paintbrush": "7.0",
+ "lower_left_paintbrush": "7.0",
+ "crayon": "7.0",
+ "lower_left_crayon": "7.0",
+ "pencil": "6.0",
+ "briefcase": "6.0",
+ "file_folder": "6.0",
+ "open_file_folder": "6.0",
+ "dividers": "7.0",
+ "card_index_dividers": "7.0",
+ "date": "6.0",
+ "calendar": "6.0",
+ "notepad_spiral": "7.0",
+ "spiral_note_pad": "7.0",
+ "calendar_spiral": "7.0",
+ "spiral_calendar_pad": "7.0",
+ "card_index": "6.0",
+ "chart_with_upwards_trend": "6.0",
+ "chart_with_downwards_trend": "6.0",
+ "bar_chart": "6.0",
+ "clipboard": "6.0",
+ "pushpin": "6.0",
+ "round_pushpin": "6.0",
+ "paperclip": "6.0",
+ "paperclips": "7.0",
+ "linked_paperclips": "7.0",
+ "straight_ruler": "6.0",
+ "triangular_ruler": "6.0",
+ "scissors": "1.1",
+ "card_box": "7.0",
+ "card_file_box": "7.0",
+ "file_cabinet": "7.0",
+ "wastebasket": "7.0",
+ "lock": "6.0",
+ "unlock": "6.0",
+ "lock_with_ink_pen": "6.0",
+ "closed_lock_with_key": "6.0",
+ "key": "6.0",
+ "key2": "7.0",
+ "old_key": "7.0",
+ "hammer": "6.0",
+ "pick": "5.2",
+ "hammer_pick": "4.1",
+ "hammer_and_pick": "4.1",
+ "tools": "7.0",
+ "hammer_and_wrench": "7.0",
+ "dagger": "7.0",
+ "dagger_knife": "7.0",
+ "crossed_swords": "4.1",
+ "gun": "6.0",
+ "bow_and_arrow": "8.0",
+ "archery": "8.0",
+ "shield": "7.0",
+ "wrench": "6.0",
+ "nut_and_bolt": "6.0",
+ "gear": "4.1",
+ "compression": "7.0",
+ "alembic": "4.1",
+ "scales": "4.1",
+ "link": "6.0",
+ "chains": "5.2",
+ "syringe": "6.0",
+ "pill": "6.0",
+ "smoking": "6.0",
+ "coffin": "4.1",
+ "urn": "4.1",
+ "funeral_urn": "4.1",
+ "moyai": "6.0",
+ "oil": "7.0",
+ "oil_drum": "7.0",
+ "crystal_ball": "6.0",
+ "shopping_cart": "9.0",
+ "shopping_trolley": "9.0",
+ "atm": "6.0",
+ "put_litter_in_its_place": "6.0",
+ "potable_water": "6.0",
+ "wheelchair": "4.1",
+ "mens": "6.0",
+ "womens": "6.0",
+ "restroom": "6.0",
+ "baby_symbol": "6.0",
+ "wc": "6.0",
+ "passport_control": "6.0",
+ "customs": "6.0",
+ "baggage_claim": "6.0",
+ "left_luggage": "6.0",
+ "warning": "4.0",
+ "children_crossing": "6.0",
+ "no_entry": "5.2",
+ "no_entry_sign": "6.0",
+ "no_bicycles": "6.0",
+ "no_smoking": "6.0",
+ "do_not_litter": "6.0",
+ "non-potable_water": "6.0",
+ "no_pedestrians": "6.0",
+ "no_mobile_phones": "6.0",
+ "underage": "6.0",
+ "radioactive": "1.1",
+ "radioactive_sign": "1.1",
+ "biohazard": "1.1",
+ "biohazard_sign": "1.1",
+ "arrow_up": "4.0",
+ "arrow_upper_right": "1.1",
+ "arrow_right": "1.1",
+ "arrow_lower_right": "1.1",
+ "arrow_down": "4.0",
+ "arrow_lower_left": "1.1",
+ "arrow_left": "4.0",
+ "arrow_upper_left": "1.1",
+ "arrow_up_down": "1.1",
+ "left_right_arrow": "1.1",
+ "leftwards_arrow_with_hook": "1.1",
+ "arrow_right_hook": "1.1",
+ "arrow_heading_up": "3.2",
+ "arrow_heading_down": "3.2",
+ "arrows_clockwise": "6.0",
+ "arrows_counterclockwise": "6.0",
+ "back": "6.0",
+ "end": "6.0",
+ "on": "6.0",
+ "soon": "6.0",
+ "top": "6.0",
+ "place_of_worship": "8.0",
+ "worship_symbol": "8.0",
+ "atom": "4.1",
+ "atom_symbol": "4.1",
+ "om_symbol": "7.0",
+ "star_of_david": "1.1",
+ "wheel_of_dharma": "1.1",
+ "yin_yang": "1.1",
+ "cross": "1.1",
+ "latin_cross": "1.1",
+ "orthodox_cross": "1.1",
+ "star_and_crescent": "1.1",
+ "peace": "1.1",
+ "peace_symbol": "1.1",
+ "menorah": "8.0",
+ "six_pointed_star": "6.0",
+ "aries": "1.1",
+ "taurus": "1.1",
+ "gemini": "1.1",
+ "cancer": "1.1",
+ "leo": "1.1",
+ "virgo": "1.1",
+ "libra": "1.1",
+ "scorpius": "1.1",
+ "sagittarius": "1.1",
+ "capricorn": "1.1",
+ "aquarius": "1.1",
+ "pisces": "1.1",
+ "ophiuchus": "6.0",
+ "twisted_rightwards_arrows": "6.0",
+ "repeat": "6.0",
+ "repeat_one": "6.0",
+ "arrow_forward": "1.1",
+ "fast_forward": "6.0",
+ "track_next": "6.0",
+ "next_track": "6.0",
+ "play_pause": "6.0",
+ "arrow_backward": "1.1",
+ "rewind": "6.0",
+ "track_previous": "6.0",
+ "previous_track": "6.0",
+ "arrow_up_small": "6.0",
+ "arrow_double_up": "6.0",
+ "arrow_down_small": "6.0",
+ "arrow_double_down": "6.0",
+ "pause_button": "7.0",
+ "double_vertical_bar": "7.0",
+ "stop_button": "7.0",
+ "record_button": "7.0",
+ "eject": "4.0",
+ "eject_symbol": "4.0",
+ "cinema": "6.0",
+ "low_brightness": "6.0",
+ "high_brightness": "6.0",
+ "signal_strength": "6.0",
+ "vibration_mode": "6.0",
+ "mobile_phone_off": "6.0",
+ "recycle": "3.2",
+ "name_badge": "6.0",
+ "fleur-de-lis": "4.1",
+ "beginner": "6.0",
+ "trident": "6.0",
+ "o": "5.2",
+ "white_check_mark": "6.0",
+ "ballot_box_with_check": "1.1",
+ "heavy_check_mark": "1.1",
+ "heavy_multiplication_x": "1.1",
+ "x": "6.0",
+ "negative_squared_cross_mark": "6.0",
+ "heavy_plus_sign": "6.0",
+ "heavy_minus_sign": "6.0",
+ "heavy_division_sign": "6.0",
+ "curly_loop": "6.0",
+ "loop": "6.0",
+ "part_alternation_mark": "3.2",
+ "eight_spoked_asterisk": "1.1",
+ "eight_pointed_black_star": "1.1",
+ "sparkle": "1.1",
+ "bangbang": "1.1",
+ "interrobang": "3.0",
+ "question": "6.0",
+ "grey_question": "6.0",
+ "grey_exclamation": "6.0",
+ "exclamation": "5.2",
+ "wavy_dash": "1.1",
+ "copyright": "1.1",
+ "registered": "1.1",
+ "tm": "1.1",
+ "hash": "3.0",
+ "asterisk": "3.0",
+ "keycap_asterisk": "3.0",
+ "zero": "3.0",
+ "one": "3.0",
+ "two": "3.0",
+ "three": "3.0",
+ "four": "3.0",
+ "five": "3.0",
+ "six": "3.0",
+ "seven": "3.0",
+ "eight": "3.0",
+ "nine": "3.0",
+ "keycap_ten": "6.0",
+ "capital_abcd": "6.0",
+ "abcd": "6.0",
+ "symbols": "6.0",
+ "abc": "6.0",
+ "a": "6.0",
+ "ab": "6.0",
+ "b": "6.0",
+ "cl": "6.0",
+ "cool": "6.0",
+ "free": "6.0",
+ "information_source": "3.0",
+ "id": "6.0",
+ "m": "1.1",
+ "new": "6.0",
+ "ng": "6.0",
+ "o2": "6.0",
+ "ok": "6.0",
+ "parking": "5.2",
+ "sos": "6.0",
+ "up": "6.0",
+ "vs": "6.0",
+ "koko": "6.0",
+ "sa": "6.0",
+ "u6708": "6.0",
+ "u6709": "6.0",
+ "u6307": "5.2",
+ "ideograph_advantage": "6.0",
+ "u5272": "6.0",
+ "u7121": "5.2",
+ "u7981": "6.0",
+ "accept": "6.0",
+ "u7533": "6.0",
+ "u5408": "6.0",
+ "u7a7a": "6.0",
+ "congratulations": "1.1",
+ "secret": "1.1",
+ "u55b6": "6.0",
+ "u6e80": "6.0",
+ "black_small_square": "1.1",
+ "white_small_square": "1.1",
+ "white_medium_square": "3.2",
+ "black_medium_square": "3.2",
+ "white_medium_small_square": "3.2",
+ "black_medium_small_square": "3.2",
+ "black_large_square": "5.1",
+ "white_large_square": "5.1",
+ "large_orange_diamond": "6.0",
+ "large_blue_diamond": "6.0",
+ "small_orange_diamond": "6.0",
+ "small_blue_diamond": "6.0",
+ "small_red_triangle": "6.0",
+ "small_red_triangle_down": "6.0",
+ "diamond_shape_with_a_dot_inside": "6.0",
+ "radio_button": "6.0",
+ "black_square_button": "6.0",
+ "white_square_button": "6.0",
+ "white_circle": "4.1",
+ "black_circle": "4.1",
+ "red_circle": "6.0",
+ "blue_circle": "6.0",
+ "checkered_flag": "6.0",
+ "triangular_flag_on_post": "6.0",
+ "crossed_flags": "6.0",
+ "flag_black": "6.0",
+ "waving_black_flag": "6.0",
+ "flag_white": "6.0",
+ "waving_white_flag": "6.0",
+ "rainbow_flag": "6.0",
+ "gay_pride_flag": "6.0",
+ "flag_ac": "6.0",
+ "ac": "6.0",
+ "flag_ad": "6.0",
+ "ad": "6.0",
+ "flag_ae": "6.0",
+ "ae": "6.0",
+ "flag_af": "6.0",
+ "af": "6.0",
+ "flag_ag": "6.0",
+ "ag": "6.0",
+ "flag_ai": "6.0",
+ "ai": "6.0",
+ "flag_al": "6.0",
+ "al": "6.0",
+ "flag_am": "6.0",
+ "am": "6.0",
+ "flag_ao": "6.0",
+ "ao": "6.0",
+ "flag_aq": "6.0",
+ "aq": "6.0",
+ "flag_ar": "6.0",
+ "ar": "6.0",
+ "flag_as": "6.0",
+ "as": "6.0",
+ "flag_at": "6.0",
+ "at": "6.0",
+ "flag_au": "6.0",
+ "au": "6.0",
+ "flag_aw": "6.0",
+ "aw": "6.0",
+ "flag_ax": "6.0",
+ "ax": "6.0",
+ "flag_az": "6.0",
+ "az": "6.0",
+ "flag_ba": "6.0",
+ "ba": "6.0",
+ "flag_bb": "6.0",
+ "bb": "6.0",
+ "flag_bd": "6.0",
+ "bd": "6.0",
+ "flag_be": "6.0",
+ "be": "6.0",
+ "flag_bf": "6.0",
+ "bf": "6.0",
+ "flag_bg": "6.0",
+ "bg": "6.0",
+ "flag_bh": "6.0",
+ "bh": "6.0",
+ "flag_bi": "6.0",
+ "bi": "6.0",
+ "flag_bj": "6.0",
+ "bj": "6.0",
+ "flag_bl": "6.0",
+ "bl": "6.0",
+ "flag_bm": "6.0",
+ "bm": "6.0",
+ "flag_bn": "6.0",
+ "bn": "6.0",
+ "flag_bo": "6.0",
+ "bo": "6.0",
+ "flag_bq": "6.0",
+ "bq": "6.0",
+ "flag_br": "6.0",
+ "br": "6.0",
+ "flag_bs": "6.0",
+ "bs": "6.0",
+ "flag_bt": "6.0",
+ "bt": "6.0",
+ "flag_bv": "6.0",
+ "bv": "6.0",
+ "flag_bw": "6.0",
+ "bw": "6.0",
+ "flag_by": "6.0",
+ "by": "6.0",
+ "flag_bz": "6.0",
+ "bz": "6.0",
+ "flag_ca": "6.0",
+ "ca": "6.0",
+ "flag_cc": "6.0",
+ "cc": "6.0",
+ "flag_cd": "6.0",
+ "congo": "6.0",
+ "flag_cf": "6.0",
+ "cf": "6.0",
+ "flag_cg": "6.0",
+ "cg": "6.0",
+ "flag_ch": "6.0",
+ "ch": "6.0",
+ "flag_ci": "6.0",
+ "ci": "6.0",
+ "flag_ck": "6.0",
+ "ck": "6.0",
+ "flag_cl": "6.0",
+ "chile": "6.0",
+ "flag_cm": "6.0",
+ "cm": "6.0",
+ "flag_cn": "6.0",
+ "cn": "6.0",
+ "flag_co": "6.0",
+ "co": "6.0",
+ "flag_cp": "6.0",
+ "cp": "6.0",
+ "flag_cr": "6.0",
+ "cr": "6.0",
+ "flag_cu": "6.0",
+ "cu": "6.0",
+ "flag_cv": "6.0",
+ "cv": "6.0",
+ "flag_cw": "6.0",
+ "cw": "6.0",
+ "flag_cx": "6.0",
+ "cx": "6.0",
+ "flag_cy": "6.0",
+ "cy": "6.0",
+ "flag_cz": "6.0",
+ "cz": "6.0",
+ "flag_de": "6.0",
+ "de": "6.0",
+ "flag_dg": "6.0",
+ "dg": "6.0",
+ "flag_dj": "6.0",
+ "dj": "6.0",
+ "flag_dk": "6.0",
+ "dk": "6.0",
+ "flag_dm": "6.0",
+ "dm": "6.0",
+ "flag_do": "6.0",
+ "do": "6.0",
+ "flag_dz": "6.0",
+ "dz": "6.0",
+ "flag_ea": "6.0",
+ "ea": "6.0",
+ "flag_ec": "6.0",
+ "ec": "6.0",
+ "flag_ee": "6.0",
+ "ee": "6.0",
+ "flag_eg": "6.0",
+ "eg": "6.0",
+ "flag_eh": "6.0",
+ "eh": "6.0",
+ "flag_er": "6.0",
+ "er": "6.0",
+ "flag_es": "6.0",
+ "es": "6.0",
+ "flag_et": "6.0",
+ "et": "6.0",
+ "flag_eu": "6.0",
+ "eu": "6.0",
+ "flag_fi": "6.0",
+ "fi": "6.0",
+ "flag_fj": "6.0",
+ "fj": "6.0",
+ "flag_fk": "6.0",
+ "fk": "6.0",
+ "flag_fm": "6.0",
+ "fm": "6.0",
+ "flag_fo": "6.0",
+ "fo": "6.0",
+ "flag_fr": "6.0",
+ "fr": "6.0",
+ "flag_ga": "6.0",
+ "ga": "6.0",
+ "flag_gb": "6.0",
+ "gb": "6.0",
+ "flag_gd": "6.0",
+ "gd": "6.0",
+ "flag_ge": "6.0",
+ "ge": "6.0",
+ "flag_gf": "6.0",
+ "gf": "6.0",
+ "flag_gg": "6.0",
+ "gg": "6.0",
+ "flag_gh": "6.0",
+ "gh": "6.0",
+ "flag_gi": "6.0",
+ "gi": "6.0",
+ "flag_gl": "6.0",
+ "gl": "6.0",
+ "flag_gm": "6.0",
+ "gm": "6.0",
+ "flag_gn": "6.0",
+ "gn": "6.0",
+ "flag_gp": "6.0",
+ "gp": "6.0",
+ "flag_gq": "6.0",
+ "gq": "6.0",
+ "flag_gr": "6.0",
+ "gr": "6.0",
+ "flag_gs": "6.0",
+ "gs": "6.0",
+ "flag_gt": "6.0",
+ "gt": "6.0",
+ "flag_gu": "6.0",
+ "gu": "6.0",
+ "flag_gw": "6.0",
+ "gw": "6.0",
+ "flag_gy": "6.0",
+ "gy": "6.0",
+ "flag_hk": "6.0",
+ "hk": "6.0",
+ "flag_hm": "6.0",
+ "hm": "6.0",
+ "flag_hn": "6.0",
+ "hn": "6.0",
+ "flag_hr": "6.0",
+ "hr": "6.0",
+ "flag_ht": "6.0",
+ "ht": "6.0",
+ "flag_hu": "6.0",
+ "hu": "6.0",
+ "flag_ic": "6.0",
+ "ic": "6.0",
+ "flag_id": "6.0",
+ "indonesia": "6.0",
+ "flag_ie": "6.0",
+ "ie": "6.0",
+ "flag_il": "6.0",
+ "il": "6.0",
+ "flag_im": "6.0",
+ "im": "6.0",
+ "flag_in": "6.0",
+ "in": "6.0",
+ "flag_io": "6.0",
+ "io": "6.0",
+ "flag_iq": "6.0",
+ "iq": "6.0",
+ "flag_ir": "6.0",
+ "ir": "6.0",
+ "flag_is": "6.0",
+ "is": "6.0",
+ "flag_it": "6.0",
+ "it": "6.0",
+ "flag_je": "6.0",
+ "je": "6.0",
+ "flag_jm": "6.0",
+ "jm": "6.0",
+ "flag_jo": "6.0",
+ "jo": "6.0",
+ "flag_jp": "6.0",
+ "jp": "6.0",
+ "flag_ke": "6.0",
+ "ke": "6.0",
+ "flag_kg": "6.0",
+ "kg": "6.0",
+ "flag_kh": "6.0",
+ "kh": "6.0",
+ "flag_ki": "6.0",
+ "ki": "6.0",
+ "flag_km": "6.0",
+ "km": "6.0",
+ "flag_kn": "6.0",
+ "kn": "6.0",
+ "flag_kp": "6.0",
+ "kp": "6.0",
+ "flag_kr": "6.0",
+ "kr": "6.0",
+ "flag_kw": "6.0",
+ "kw": "6.0",
+ "flag_ky": "6.0",
+ "ky": "6.0",
+ "flag_kz": "6.0",
+ "kz": "6.0",
+ "flag_la": "6.0",
+ "la": "6.0",
+ "flag_lb": "6.0",
+ "lb": "6.0",
+ "flag_lc": "6.0",
+ "lc": "6.0",
+ "flag_li": "6.0",
+ "li": "6.0",
+ "flag_lk": "6.0",
+ "lk": "6.0",
+ "flag_lr": "6.0",
+ "lr": "6.0",
+ "flag_ls": "6.0",
+ "ls": "6.0",
+ "flag_lt": "6.0",
+ "lt": "6.0",
+ "flag_lu": "6.0",
+ "lu": "6.0",
+ "flag_lv": "6.0",
+ "lv": "6.0",
+ "flag_ly": "6.0",
+ "ly": "6.0",
+ "flag_ma": "6.0",
+ "ma": "6.0",
+ "flag_mc": "6.0",
+ "mc": "6.0",
+ "flag_md": "6.0",
+ "md": "6.0",
+ "flag_me": "6.0",
+ "me": "6.0",
+ "flag_mf": "6.0",
+ "mf": "6.0",
+ "flag_mg": "6.0",
+ "mg": "6.0",
+ "flag_mh": "6.0",
+ "mh": "6.0",
+ "flag_mk": "6.0",
+ "mk": "6.0",
+ "flag_ml": "6.0",
+ "ml": "6.0",
+ "flag_mm": "6.0",
+ "mm": "6.0",
+ "flag_mn": "6.0",
+ "mn": "6.0",
+ "flag_mo": "6.0",
+ "mo": "6.0",
+ "flag_mp": "6.0",
+ "mp": "6.0",
+ "flag_mq": "6.0",
+ "mq": "6.0",
+ "flag_mr": "6.0",
+ "mr": "6.0",
+ "flag_ms": "6.0",
+ "ms": "6.0",
+ "flag_mt": "6.0",
+ "mt": "6.0",
+ "flag_mu": "6.0",
+ "mu": "6.0",
+ "flag_mv": "6.0",
+ "mv": "6.0",
+ "flag_mw": "6.0",
+ "mw": "6.0",
+ "flag_mx": "6.0",
+ "mx": "6.0",
+ "flag_my": "6.0",
+ "my": "6.0",
+ "flag_mz": "6.0",
+ "mz": "6.0",
+ "flag_na": "6.0",
+ "na": "6.0",
+ "flag_nc": "6.0",
+ "nc": "6.0",
+ "flag_ne": "6.0",
+ "ne": "6.0",
+ "flag_nf": "6.0",
+ "nf": "6.0",
+ "flag_ng": "6.0",
+ "nigeria": "6.0",
+ "flag_ni": "6.0",
+ "ni": "6.0",
+ "flag_nl": "6.0",
+ "nl": "6.0",
+ "flag_no": "6.0",
+ "no": "6.0",
+ "flag_np": "6.0",
+ "np": "6.0",
+ "flag_nr": "6.0",
+ "nr": "6.0",
+ "flag_nu": "6.0",
+ "nu": "6.0",
+ "flag_nz": "6.0",
+ "nz": "6.0",
+ "flag_om": "6.0",
+ "om": "6.0",
+ "flag_pa": "6.0",
+ "pa": "6.0",
+ "flag_pe": "6.0",
+ "pe": "6.0",
+ "flag_pf": "6.0",
+ "pf": "6.0",
+ "flag_pg": "6.0",
+ "pg": "6.0",
+ "flag_ph": "6.0",
+ "ph": "6.0",
+ "flag_pk": "6.0",
+ "pk": "6.0",
+ "flag_pl": "6.0",
+ "pl": "6.0",
+ "flag_pm": "6.0",
+ "pm": "6.0",
+ "flag_pn": "6.0",
+ "pn": "6.0",
+ "flag_pr": "6.0",
+ "pr": "6.0",
+ "flag_ps": "6.0",
+ "ps": "6.0",
+ "flag_pt": "6.0",
+ "pt": "6.0",
+ "flag_pw": "6.0",
+ "pw": "6.0",
+ "flag_py": "6.0",
+ "py": "6.0",
+ "flag_qa": "6.0",
+ "qa": "6.0",
+ "flag_re": "6.0",
+ "re": "6.0",
+ "flag_ro": "6.0",
+ "ro": "6.0",
+ "flag_rs": "6.0",
+ "rs": "6.0",
+ "flag_ru": "6.0",
+ "ru": "6.0",
+ "flag_rw": "6.0",
+ "rw": "6.0",
+ "flag_sa": "6.0",
+ "saudiarabia": "6.0",
+ "saudi": "6.0",
+ "flag_sb": "6.0",
+ "sb": "6.0",
+ "flag_sc": "6.0",
+ "sc": "6.0",
+ "flag_sd": "6.0",
+ "sd": "6.0",
+ "flag_se": "6.0",
+ "se": "6.0",
+ "flag_sg": "6.0",
+ "sg": "6.0",
+ "flag_sh": "6.0",
+ "sh": "6.0",
+ "flag_si": "6.0",
+ "si": "6.0",
+ "flag_sj": "6.0",
+ "sj": "6.0",
+ "flag_sk": "6.0",
+ "sk": "6.0",
+ "flag_sl": "6.0",
+ "sl": "6.0",
+ "flag_sm": "6.0",
+ "sm": "6.0",
+ "flag_sn": "6.0",
+ "sn": "6.0",
+ "flag_so": "6.0",
+ "so": "6.0",
+ "flag_sr": "6.0",
+ "sr": "6.0",
+ "flag_ss": "6.0",
+ "ss": "6.0",
+ "flag_st": "6.0",
+ "st": "6.0",
+ "flag_sv": "6.0",
+ "sv": "6.0",
+ "flag_sx": "6.0",
+ "sx": "6.0",
+ "flag_sy": "6.0",
+ "sy": "6.0",
+ "flag_sz": "6.0",
+ "sz": "6.0",
+ "flag_ta": "6.0",
+ "ta": "6.0",
+ "flag_tc": "6.0",
+ "tc": "6.0",
+ "flag_td": "6.0",
+ "td": "6.0",
+ "flag_tf": "6.0",
+ "tf": "6.0",
+ "flag_tg": "6.0",
+ "tg": "6.0",
+ "flag_th": "6.0",
+ "th": "6.0",
+ "flag_tj": "6.0",
+ "tj": "6.0",
+ "flag_tk": "6.0",
+ "tk": "6.0",
+ "flag_tl": "6.0",
+ "tl": "6.0",
+ "flag_tm": "6.0",
+ "turkmenistan": "6.0",
+ "flag_tn": "6.0",
+ "tn": "6.0",
+ "flag_to": "6.0",
+ "to": "6.0",
+ "flag_tr": "6.0",
+ "tr": "6.0",
+ "flag_tt": "6.0",
+ "tt": "6.0",
+ "flag_tv": "6.0",
+ "tuvalu": "6.0",
+ "flag_tw": "6.0",
+ "tw": "6.0",
+ "flag_tz": "6.0",
+ "tz": "6.0",
+ "flag_ua": "6.0",
+ "ua": "6.0",
+ "flag_ug": "6.0",
+ "ug": "6.0",
+ "flag_um": "6.0",
+ "um": "6.0",
+ "flag_us": "6.0",
+ "us": "6.0",
+ "flag_uy": "6.0",
+ "uy": "6.0",
+ "flag_uz": "6.0",
+ "uz": "6.0",
+ "flag_va": "6.0",
+ "va": "6.0",
+ "flag_vc": "6.0",
+ "vc": "6.0",
+ "flag_ve": "6.0",
+ "ve": "6.0",
+ "flag_vg": "6.0",
+ "vg": "6.0",
+ "flag_vi": "6.0",
+ "vi": "6.0",
+ "flag_vn": "6.0",
+ "vn": "6.0",
+ "flag_vu": "6.0",
+ "vu": "6.0",
+ "flag_wf": "6.0",
+ "wf": "6.0",
+ "flag_ws": "6.0",
+ "ws": "6.0",
+ "flag_xk": "6.0",
+ "xk": "6.0",
+ "flag_ye": "6.0",
+ "ye": "6.0",
+ "flag_yt": "6.0",
+ "yt": "6.0",
+ "flag_za": "6.0",
+ "za": "6.0",
+ "flag_zm": "6.0",
+ "zm": "6.0",
+ "flag_zw": "6.0",
+ "zw": "6.0",
+ "regional_indicator_z": "6.0",
+ "regional_indicator_y": "6.0",
+ "regional_indicator_x": "6.0",
+ "regional_indicator_w": "6.0",
+ "regional_indicator_v": "6.0",
+ "regional_indicator_u": "6.0",
+ "regional_indicator_t": "6.0",
+ "regional_indicator_s": "6.0",
+ "regional_indicator_r": "6.0",
+ "regional_indicator_q": "6.0",
+ "regional_indicator_p": "6.0",
+ "regional_indicator_o": "6.0",
+ "regional_indicator_n": "6.0",
+ "regional_indicator_m": "6.0",
+ "regional_indicator_l": "6.0",
+ "regional_indicator_k": "6.0",
+ "regional_indicator_j": "6.0",
+ "regional_indicator_i": "6.0",
+ "regional_indicator_h": "6.0",
+ "regional_indicator_g": "6.0",
+ "regional_indicator_f": "6.0",
+ "regional_indicator_e": "6.0",
+ "regional_indicator_d": "6.0",
+ "regional_indicator_c": "6.0",
+ "regional_indicator_b": "6.0",
+ "regional_indicator_a": "6.0",
+ "large_blue_circle": "6.0",
+ "ten": "6.0"
+} \ No newline at end of file
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 789f45489eb..a5c9f0b509c 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ed775f898d2..7c7bfada7d0 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -5,26 +5,42 @@ module API
version %w(v3 v4), using: :path
version 'v3', using: :path do
+ helpers ::API::V3::Helpers
+
+ mount ::API::V3::AwardEmoji
mount ::API::V3::Boards
mount ::API::V3::Branches
+ mount ::API::V3::BroadcastMessages
+ mount ::API::V3::Builds
mount ::API::V3::Commits
mount ::API::V3::DeployKeys
+ mount ::API::V3::Environments
mount ::API::V3::Files
+ mount ::API::V3::Groups
mount ::API::V3::Issues
mount ::API::V3::Labels
mount ::API::V3::Members
mount ::API::V3::MergeRequestDiffs
mount ::API::V3::MergeRequests
+ mount ::API::V3::Notes
+ mount ::API::V3::Pipelines
mount ::API::V3::ProjectHooks
+ mount ::API::V3::Milestones
mount ::API::V3::Projects
mount ::API::V3::ProjectSnippets
mount ::API::V3::Repositories
+ mount ::API::V3::Runners
+ mount ::API::V3::Services
+ mount ::API::V3::Settings
+ mount ::API::V3::Snippets
mount ::API::V3::Subscriptions
mount ::API::V3::SystemHooks
mount ::API::V3::Tags
- mount ::API::V3::Todos
mount ::API::V3::Templates
+ mount ::API::V3::Todos
+ mount ::API::V3::Triggers
mount ::API::V3::Users
+ mount ::API::V3::Variables
end
before { allow_access_with_scope :api }
@@ -47,6 +63,10 @@ module API
error! e.message, e.status, e.headers
end
+ rescue_from Gitlab::Auth::TooManyIps do |e|
+ rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
@@ -64,7 +84,6 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
- mount ::API::Builds
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DeployKeys
@@ -74,6 +93,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Jobs
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
@@ -90,6 +110,7 @@ module API
mount ::API::Projects
mount ::API::ProjectSnippets
mount ::API::Repositories
+ mount ::API::Runner
mount ::API::Runners
mount ::API::Services
mount ::API::Session
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index df6db140d0e..409cb5b924f 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,7 +6,7 @@ module API
module APIGuard
extend ActiveSupport::Concern
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
PRIVATE_TOKEN_PARAM = :private_token
included do |base|
@@ -114,8 +114,8 @@ module API
private
def install_error_responders(base)
- error_classes = [ MissingTokenError, TokenNotFoundError,
- ExpiredError, RevokedError, InsufficientScopeError]
+ error_classes = [MissingTokenError, TokenNotFoundError,
+ ExpiredError, RevokedError, InsufficientScopeError]
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
end
@@ -160,13 +160,10 @@ module API
# Exceptions
#
- class MissingTokenError < StandardError; end
-
- class TokenNotFoundError < StandardError; end
-
- class ExpiredError < StandardError; end
-
- class RevokedError < StandardError; end
+ MissingTokenError = Class.new(StandardError)
+ TokenNotFoundError = Class.new(StandardError)
+ ExpiredError = Class.new(StandardError)
+ RevokedError = Class.new(StandardError)
class InsufficientScopeError < StandardError
attr_reader :scopes
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2ef327217ea..56f19f89642 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -3,19 +3,26 @@ module API
include PaginationParams
before { authenticate! }
- AWARDABLES = %w[issue merge_request snippet]
-
- resource :projects do
- AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.pluralize
- awardable_id_string = "#{awardable_type}_id"
+ AWARDABLES = [
+ { type: 'issue', find_by: :iid },
+ { type: 'merge_request', find_by: :iid },
+ { type: 'snippet', find_by: :id }
+ ].freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ AWARDABLES.each do |awardable_params|
+ awardable_string = awardable_params[:type].pluralize
+ awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
- [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ [
+ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
@@ -82,7 +89,6 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
award.destroy
- present award, with: Entities::AwardEmoji
end
end
end
@@ -104,10 +110,10 @@ module API
note_id = params.delete(:note_id)
awardable.notes.find(note_id)
- elsif params.include?(:issue_id)
- user_project.issues.find(params[:issue_id])
- elsif params.include?(:merge_request_id)
- user_project.merge_requests.find(params[:merge_request_id])
+ elsif params.include?(:issue_iid)
+ user_project.issues.find_by!(iid: params[:issue_iid])
+ elsif params.include?(:merge_request_iid)
+ user_project.merge_requests.find_by!(iid: params[:merge_request_iid])
else
user_project.snippets.find(params[:snippet_id])
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index f4226e5a89d..5a2d7a681e3 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
@@ -127,9 +127,7 @@ module API
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
- if service.execute(list)
- present list, with: Entities::List
- else
+ unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index c65de90cca2..f35084a582a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -4,13 +4,12 @@ module API
class Branches < Grape::API
include PaginationParams
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success Entities::RepoBranch
end
@@ -102,6 +101,7 @@ module API
end
post ":id/repository/branches" do
authorize_push_project
+
result = CreateBranchService.new(user_project, current_user).
execute(params[:branch], params[:ref])
@@ -124,11 +124,7 @@ module API
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
- if result[:status] == :success
- {
- branch: params[:branch]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
@@ -137,7 +133,7 @@ module API
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
- status(200)
+ accepted!
end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 1217002bf8e..395c401203c 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -91,7 +91,7 @@ module API
delete ':id' do
message = find_message
- present message.destroy, with: Entities::BroadcastMessage
+ message.destroy
end
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
deleted file mode 100644
index 44fe0fc4a95..00000000000
--- a/lib/api/builds.rb
+++ /dev/null
@@ -1,261 +0,0 @@
-module API
- class Builds < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects do
- helpers do
- params :optional_scope do
- optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
- values: ['pending', 'running', 'failed', 'success', 'canceled'],
- coerce_with: ->(scope) {
- if scope.is_a?(String)
- [scope]
- elsif scope.is_a?(Hashie::Mash)
- scope.values
- else
- ['unknown']
- end
- }
- end
- end
-
- desc 'Get a project builds' do
- success Entities::Build
- end
- params do
- use :optional_scope
- use :pagination
- end
- get ':id/builds' do
- builds = user_project.builds.order('id DESC')
- builds = filter_builds(builds, params[:scope])
-
- present paginate(builds), with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Get builds for a specific commit of a project' do
- success Entities::Build
- end
- params do
- requires :sha, type: String, desc: 'The SHA id of a commit'
- use :optional_scope
- use :pagination
- end
- get ':id/repository/commits/:sha/builds' do
- authorize_read_builds!
-
- return not_found! unless user_project.commit(params[:sha])
-
- pipelines = user_project.pipelines.where(sha: params[:sha])
- builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
- builds = filter_builds(builds, params[:scope])
-
- present paginate(builds), with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Get a specific build of a project' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Download the artifacts file from build' do
- detail 'This feature was introduced in GitLab 8.5'
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id/artifacts' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- present_artifacts!(build.artifacts_file)
- end
-
- desc 'Download the artifacts file from build' do
- detail 'This feature was introduced in GitLab 8.10'
- end
- params do
- requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the build'
- end
- get ':id/builds/artifacts/:ref_name/download',
- requirements: { ref_name: /.+/ } do
- authorize_read_builds!
-
- builds = user_project.latest_successful_builds_for(params[:ref_name])
- latest_build = builds.find_by!(name: params[:job])
-
- present_artifacts!(latest_build.artifacts_file)
- end
-
- # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
- # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
- # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
- desc 'Get a trace of a specific build of a project'
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- get ':id/builds/:build_id/trace' do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
- content_type 'text/plain'
- env['api.format'] = :binary
-
- trace = build.trace
- body trace
- end
-
- desc 'Cancel a specific build of a project' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/cancel' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
-
- build.cancel
-
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Retry a specific build of a project' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/retry' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- return forbidden!('Build is not retryable') unless build.retryable?
-
- build = Ci::Build.retry(build, current_user)
-
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Erase build (remove artifacts and build trace)' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/erase' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- return forbidden!('Build is not erasable!') unless build.erasable?
-
- build.erase(erased_by: current_user)
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
- end
-
- desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Build
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
- end
- post ':id/builds/:build_id/artifacts/keep' do
- authorize_update_builds!
-
- build = get_build!(params[:build_id])
- return not_found!(build) unless build.artifacts?
-
- build.keep_artifacts!
-
- status 200
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
-
- desc 'Trigger a manual build' do
- success Entities::Build
- detail 'This feature was added in GitLab 8.11'
- end
- params do
- requires :build_id, type: Integer, desc: 'The ID of a Build'
- end
- post ":id/builds/:build_id/play" do
- authorize_read_builds!
-
- build = get_build!(params[:build_id])
-
- bad_request!("Unplayable Job") unless build.playable?
-
- build.play(current_user)
-
- status 200
- present build, with: Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
- end
- end
-
- helpers do
- def get_build(id)
- user_project.builds.find_by(id: id.to_i)
- end
-
- def get_build!(id)
- get_build(id) || not_found!
- end
-
- def present_artifacts!(artifacts_file)
- if !artifacts_file.file_storage?
- redirect_to(build.artifacts_file.url)
- elsif artifacts_file.exists?
- present_file!(artifacts_file.path, artifacts_file.filename)
- else
- not_found!
- end
- end
-
- def filter_builds(builds, scope)
- return builds if scope.nil? || scope.empty?
-
- available_statuses = ::CommitStatus::AVAILABLE_STATUSES
-
- unknown = scope - available_statuses
- render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
-
- builds.where(status: available_statuses && scope)
- end
-
- def authorize_read_builds!
- authorize! :read_build, user_project
- end
-
- def authorize_update_builds!
- authorize! :update_build, user_project
- end
- end
- end
-end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 0b6076bd28c..827a38d33da 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -2,7 +2,10 @@ require 'mime/types'
module API
class CommitStatuses < Grape::API
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
include PaginationParams
before { authenticate! }
@@ -11,7 +14,6 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
optional :ref, type: String, desc: 'The ref'
optional :stage, type: String, desc: 'The stage'
@@ -37,10 +39,9 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
requires :state, type: String, desc: 'The state of the status',
- values: ['pending', 'running', 'success', 'failed', 'canceled']
+ values: %w(pending running success failed canceled)
optional :ref, type: String, desc: 'The ref'
optional :target_url, type: String, desc: 'The target URL to associate with this status'
optional :description, type: String, desc: 'A short description of the status'
@@ -72,14 +73,15 @@ module API
status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
project: @project,
pipeline: pipeline,
- user: current_user,
name: name,
ref: ref,
- target_url: params[:target_url],
- description: params[:description],
- coverage: params[:coverage]
+ user: current_user
)
+ optional_attributes =
+ attributes_for_keys(%w[target_url description coverage])
+
+ status.update(optional_attributes) if optional_attributes.any?
render_validation_error!(status) if status.invalid?
begin
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 0cd817f9352..66b37fd2bcc 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
success Entities::RepoCommit
end
@@ -18,22 +18,34 @@ module API
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
- optional :page, type: Integer, default: 0, desc: 'The page for pagination'
- optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
optional :path, type: String, desc: 'The file path'
+ use :pagination
end
get ":id/repository/commits" do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- offset = params[:page] * params[:per_page]
+ path = params[:path]
+ before = params[:until]
+ after = params[:since]
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ offset = (params[:page] - 1) * params[:per_page]
commits = user_project.repository.commits(ref,
- path: params[:path],
+ path: path,
limit: params[:per_page],
offset: offset,
- after: params[:since],
- before: params[:until])
+ before: before,
+ after: after)
+
+ commit_count =
+ if path || before || after
+ user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+ else
+ # Cacheable commit count.
+ user_project.repository.commit_count_for_ref(ref)
+ end
+
+ paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present commits, with: Entities::RepoCommit
+ present paginate(paginated_commits), with: Entities::RepoCommit
end
desc 'Commit multiple file changes as one commit' do
@@ -52,13 +64,6 @@ module API
attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
- attrs[:actions].map! do |action|
- action[:action] = action[:action].to_sym
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
- action
- end
-
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
@@ -134,7 +139,7 @@ module API
commit_params = {
commit: commit,
- create_merge_request: false,
+ start_branch: params[:branch],
target_branch: params[:branch]
}
@@ -157,7 +162,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
- requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+ requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 69e85c27a65..b888ede6fe8 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -17,7 +17,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc "Get a specific project's deploy keys" do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index c5feb49b22f..46b936897f6 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -1,5 +1,5 @@
module API
- # Deployments RESTfull API endpoints
+ # Deployments RESTful API endpoints
class Deployments < Grape::API
include PaginationParams
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 400ee7c92aa..5954aea8041 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -49,7 +49,8 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :pipeline_events, :wiki_page_events
+ expose :build_events, as: :job_events
end
class BasicProjectDetails < Grape::Entity
@@ -69,9 +70,8 @@ module API
class Project < Grape::Entity
expose :id, :description, :default_branch, :tag_list
- expose :public?, as: :public
expose :archived?, as: :archived
- expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
@@ -81,7 +81,7 @@ module API
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
@@ -94,11 +94,11 @@ module API
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :public_builds
+ expose :public_builds, as: :public_jobs
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
- expose :only_allow_merge_if_build_succeeds
+ expose :only_allow_merge_if_pipeline_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
@@ -110,7 +110,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
class Member < UserBasic
@@ -132,7 +132,7 @@ module API
end
class Group < Grape::Entity
- expose :id, :name, :path, :description, :visibility_level
+ expose :id, :name, :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url
expose :web_url
@@ -145,7 +145,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :build_artifacts_size, as: :job_artifacts_size
end
end
end
@@ -250,14 +250,11 @@ module API
expose :start_date
end
- class Issue < ProjectEntity
+ class IssueBasic < ProjectEntity
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
- expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
@@ -268,6 +265,12 @@ module API
end
end
+ class Issue < IssueBasic
+ expose :subscribed do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
+ end
+
class IssuableTimeStats < Grape::Entity
expose :time_estimate
expose :total_time_spent
@@ -280,7 +283,7 @@ module API
expose :id
end
- class MergeRequest < ProjectEntity
+ class MergeRequestBasic < ProjectEntity
expose :target_branch, :source_branch
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
@@ -288,13 +291,10 @@ module API
expose :label_names, as: :labels
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
- expose :merge_when_build_succeeds
+ expose :merge_when_pipeline_succeeds
expose :merge_status
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
- expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -304,6 +304,12 @@ module API
end
end
+ class MergeRequest < MergeRequestBasic
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+ end
+
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
compare.raw_diffs(all_diffs: true).to_a
@@ -339,9 +345,6 @@ module API
expose :created_at, :updated_at
expose :system?, as: :system
expose :noteable_id, :noteable_type
- # upvote? and downvote? are deprecated, always return false
- expose(:upvote?) { |note| false }
- expose(:downvote?) { |note| false }
end
class AwardEmoji < Grape::Entity
@@ -397,7 +400,8 @@ module API
expose :target_type
expose :target do |todo, options|
- Entities.const_get(todo.target_type).represent(todo.target, options)
+ target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type
+ Entities.const_get(target).represent(todo.target, options)
end
expose :target_url do |todo, options|
@@ -451,7 +455,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ expose :tag_push_events, :note_events, :pipeline_events
+ expose :build_events, as: :job_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -554,12 +559,15 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
- expose :restricted_visibility_levels
+ expose(:restricted_visibility_levels) do |setting, _options|
+ setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
+ end
expose :max_attachment_size
expose :session_expire_delay
- expose :default_project_visibility
- expose :default_snippet_visibility
- expose :default_group_visibility
+ expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
+ expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
+ expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
+ expose :default_artifacts_expire_in
expose :domain_whitelist
expose :domain_blacklist_enabled
expose :domain_blacklist
@@ -592,10 +600,6 @@ module API
end
end
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- end
-
class Runner < Grape::Entity
expose :id
expose :description
@@ -620,7 +624,11 @@ module API
end
end
- class BuildArtifactFile < Grape::Entity
+ class RunnerRegistrationDetails < Grape::Entity
+ expose :id, :token
+ end
+
+ class JobArtifactFile < Grape::Entity
expose :filename, :size
end
@@ -628,18 +636,21 @@ module API
expose :id, :sha, :ref, :status
end
- class Build < Grape::Entity
+ class Job < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
class Trigger < Grape::Entity
- expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :id
+ expose :token, :description
+ expose :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: Entities::UserBasic
end
class Variable < Grape::Entity
@@ -660,14 +671,14 @@ module API
end
class Environment < EnvironmentBasic
- expose :project, using: Entities::Project
+ expose :project, using: Entities::BasicProjectDetails
end
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
- expose :deployable, using: Entities::Build
+ expose :deployable, using: Entities::Job
end
class RepoLicense < Grape::Entity
@@ -694,5 +705,99 @@ module API
expose :id, :message, :starts_at, :ends_at, :color, :font
expose :active?, as: :active
end
+
+ class PersonalAccessToken < Grape::Entity
+ expose :id, :name, :revoked, :created_at, :scopes
+ expose :active?, as: :active
+ expose :expires_at do |personal_access_token|
+ personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
+ end
+ end
+
+ class PersonalAccessTokenWithToken < PersonalAccessToken
+ expose :token
+ end
+
+ class ImpersonationToken < PersonalAccessTokenWithToken
+ expose :impersonation
+ end
+
+ module JobRequest
+ class JobInfo < Grape::Entity
+ expose :name, :stage
+ expose :project_id, :project_name
+ end
+
+ class GitInfo < Grape::Entity
+ expose :repo_url, :ref, :sha, :before_sha
+ expose :ref_type do |model|
+ if model.tag
+ 'tag'
+ else
+ 'branch'
+ end
+ end
+ end
+
+ class RunnerInfo < Grape::Entity
+ expose :timeout
+ end
+
+ class Step < Grape::Entity
+ expose :name, :script, :timeout, :when, :allow_failure
+ end
+
+ class Image < Grape::Entity
+ expose :name
+ end
+
+ class Artifacts < Grape::Entity
+ expose :name, :untracked, :paths, :when, :expire_in
+ end
+
+ class Cache < Grape::Entity
+ expose :key, :untracked, :paths
+ end
+
+ class Credentials < Grape::Entity
+ expose :type, :url, :username, :password
+ end
+
+ class ArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Dependency < Grape::Entity
+ expose :id, :name, :token
+ expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
+ end
+
+ class Response < Grape::Entity
+ expose :id
+ expose :token
+ expose :allow_git_fetch
+
+ expose :job_info, using: JobInfo do |model|
+ model
+ end
+
+ expose :git_info, using: GitInfo do |model|
+ model
+ end
+
+ expose :runner_info, using: RunnerInfo do |model|
+ model
+ end
+
+ expose :variables
+ expose :steps, using: Step
+ expose :image, using: Image
+ expose :services, using: Image
+ expose :artifacts, using: Artifacts
+ expose :cache, using: Cache
+ expose :credentials, using: Credentials
+ expose :dependencies, using: Dependency
+ end
+ end
end
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 1a7e68f0528..945771d46f3 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
@@ -79,7 +79,24 @@ module API
environment = user_project.environments.find(params[:environment_id])
- present environment.destroy, with: Entities::Environment
+ environment.destroy
+ end
+
+ desc 'Stops an existing environment' do
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ post ':id/environments/:environment_id/stop' do
+ authorize! :create_deployment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ environment.stop_with_action!(current_user)
+
+ status 200
+ present environment, with: Entities::Environment
end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 500f9d3c787..33fc970dc09 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -14,6 +14,19 @@ module API
}
end
+ def assign_file_vars!
+ authorize! :download_code, user_project
+
+ @commit = user_project.commit(params[:ref])
+ not_found!('Commit') unless @commit
+
+ @repo = user_project.repository
+ @blob = @repo.blob_at(@commit.sha, params[:file_path])
+
+ not_found!('File') unless @blob
+ @blob.load_all_data!(@repo)
+ end
+
def commit_response(attrs)
{
file_path: attrs[:file_path],
@@ -22,7 +35,7 @@ module API
end
params :simple_file_params do
- requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
requires :branch, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit Message'
optional :author_email, type: String, desc: 'The email of the author'
@@ -39,35 +52,36 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
- desc 'Get a file from repository'
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get raw file contents from the repository'
params do
- requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
- requires :ref, type: String, desc: 'The name of branch, tag, or commit'
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag commit'
end
- get ":id/repository/files" do
- authorize! :download_code, user_project
-
- commit = user_project.commit(params[:ref])
- not_found!('Commit') unless commit
+ get ":id/repository/files/:file_path/raw" do
+ assign_file_vars!
- repo = user_project.repository
- blob = repo.blob_at(commit.sha, params[:file_path])
- not_found!('File') unless blob
+ send_git_blob @repo, @blob
+ end
- blob.load_all_data!(repo)
- status(200)
+ desc 'Get a file from the repository'
+ params do
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+ get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ assign_file_vars!
{
- file_name: blob.name,
- file_path: blob.path,
- size: blob.size,
+ file_name: @blob.name,
+ file_path: @blob.path,
+ size: @blob.size,
encoding: "base64",
- content: Base64.strict_encode64(blob.data),
+ content: Base64.strict_encode64(@blob.data),
ref: params[:ref],
- blob_id: blob.id,
- commit_id: commit.id,
- last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
+ blob_id: @blob.id,
+ commit_id: @commit.id,
+ last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
}
end
@@ -75,7 +89,7 @@ module API
params do
use :extended_file_params
end
- post ":id/repository/files" do
+ post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -93,7 +107,7 @@ module API
params do
use :extended_file_params
end
- put ":id/repository/files" do
+ put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -112,16 +126,13 @@ module API
params do
use :simple_file_params
end
- delete ":id/repository/files" do
+ delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
- if result[:status] == :success
- status(200)
- commit_response(file_params)
- else
+ if result[:status] != :success
render_api_error!(result[:message], 400)
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9f29c4466ab..8f3799417e3 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -7,7 +7,7 @@ module API
helpers do
params :optional_params do
optional :description, type: String, desc: 'The description of the group'
- optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
end
@@ -36,12 +36,15 @@ module API
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
get do
- groups = if current_user.admin
+ groups = if params[:owned]
+ current_user.owned_groups
+ elsif current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new.execute(current_user)
@@ -56,17 +59,6 @@ module API
present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end
- desc 'Get list of owned groups for authenticated user' do
- success Entities::Group
- end
- params do
- use :pagination
- use :statistics_params
- end
- get '/owned' do
- present_groups current_user.owned_groups, statistics: params[:statistics]
- end
-
desc 'Create a group. Available only for users who can create groups.' do
success Entities::Group
end
@@ -92,7 +84,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
@@ -100,7 +92,7 @@ module API
optional :name, type: String, desc: 'The name of the group'
optional :path, type: String, desc: 'The path of the group'
use :optional_params
- at_least_one_of :name, :path, :description, :visibility_level,
+ at_least_one_of :name, :path, :description, :visibility,
:lfs_enabled, :request_access_enabled
end
put ':id' do
@@ -134,7 +126,7 @@ module API
end
params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
@@ -162,7 +154,7 @@ module API
params do
requires :project_id, type: String, desc: 'The ID or path of the project'
end
- post ":id/projects/:project_id" do
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
authenticated_as_admin!
group = find_group!(params[:id])
project = find_project!(params[:project_id])
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 0fd2b1587e3..3c173b544aa 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,7 +3,7 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- SUDO_HEADER = "HTTP_SUDO"
+ SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
def declared_params(options = {})
@@ -82,22 +82,22 @@ module API
label || not_found!('Label')
end
- def find_project_issue(id)
- IssuesFinder.new(current_user, project_id: user_project.id).find(id)
+ def find_project_issue(iid)
+ IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
- def find_project_merge_request(id)
- MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ def find_project_merge_request(iid)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
- def find_merge_request_with_access(id, access_level = :read_merge_request)
- merge_request = user_project.merge_requests.find(id)
+ def find_merge_request_with_access(iid, access_level = :read_merge_request)
+ merge_request = user_project.merge_requests.find_by!(iid: iid)
authorize! access_level, merge_request
merge_request
end
def authenticate!
- unauthorized! unless current_user
+ unauthorized! unless current_user && can?(current_user, :access_api)
end
def authenticate_non_get!
@@ -126,7 +126,7 @@ module API
forbidden! unless current_user.is_admin?
end
- def authorize!(action, subject = nil)
+ def authorize!(action, subject = :global)
forbidden! unless can?(current_user, action, subject)
end
@@ -144,7 +144,7 @@ module API
end
end
- def can?(object, action, subject)
+ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -174,6 +174,10 @@ module API
items.where(iid: iid)
end
+ def filter_by_search(items, text)
+ items.search(text)
+ end
+
# error helpers
def forbidden!(reason = nil)
@@ -219,6 +223,10 @@ module API
render_api_error!('204 No Content', 204)
end
+ def accepted!
+ render_api_error!('202 Accepted', 202)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
@@ -254,6 +262,10 @@ module API
# project helpers
def filter_projects(projects)
+ if params[:membership]
+ projects = projects.merge(current_user.authorized_projects)
+ end
+
if params[:owned]
projects = projects.merge(current_user.owned_projects)
end
@@ -334,16 +346,17 @@ module API
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+ @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
+ @initial_current_user ||= find_user_from_warden
- @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
- @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
- @initial_current_user ||= find_user_from_warden
+ unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
+ @initial_current_user = nil
+ end
- unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
- @initial_current_user = nil
+ @initial_current_user
end
-
- @initial_current_user
end
def sudo!
@@ -386,14 +399,6 @@ module API
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
end
- def issue_entity(project)
- if project.has_external_issue_tracker?
- Entities::ExternalIssue
- else
- Entities::Issue
- end
- end
-
# The Grape Error Middleware only has access to env but no params. We workaround this by
# defining a method that returns the right value.
def define_params_for_grape_middleware
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 080a6274957..2135a787b11 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -9,11 +9,11 @@ module API
# In addition, they may have a '.git' extension and multiple namespaces
#
# Transform all these cases to 'namespace/project'
- def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values)
project_path = project_path.sub(/\.git\z/, '')
- storage_paths.each do |storage_path|
- storage_path = File.expand_path(storage_path)
+ storages.each do |storage|
+ storage_path = File.expand_path(storage['path'])
if project_path.start_with?(storage_path)
project_path = project_path.sub(storage_path, '')
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 00000000000..74848a6e144
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -0,0 +1,69 @@
+module API
+ module Helpers
+ module Runner
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+ JOB_TOKEN_PARAM = :token
+ UPDATE_RUNNER_EVERY = 10 * 60
+
+ def runner_registration_token_valid?
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
+ current_application_settings.runners_registration_token)
+ end
+
+ def get_runner_version_from_params
+ return unless params['info'].present?
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def current_runner
+ @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
+ end
+
+ def update_runner_info
+ return unless update_runner?
+
+ current_runner.contacted_at = Time.now
+ current_runner.assign_attributes(get_runner_version_from_params)
+ current_runner.save if current_runner.changed?
+ end
+
+ def update_runner?
+ # Use a random threshold to prevent beating DB updates.
+ # It generates a distribution between [40m, 80m].
+ #
+ contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
+
+ current_runner.contacted_at.nil? ||
+ (Time.now - current_runner.contacted_at) >= contacted_at_max_age
+ end
+
+ def validate_job!(job)
+ not_found! unless job
+
+ yield if block_given?
+
+ forbidden!('Project has been deleted!') unless job.project
+ forbidden!('Job has been erased!') if job.erased?
+ end
+
+ def authenticate_job!(job)
+ validate_job!(job) do
+ forbidden! unless job_token_valid?(job)
+ end
+ end
+
+ def job_token_valid?(job)
+ token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
+ token && job.valid_token?(token)
+ end
+
+ def max_artifacts_size
+ current_application_settings.max_artifacts_size.megabytes.to_i
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d235977fbd8..7eed93aba00 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -132,6 +132,18 @@ module API
{ success: true, recovery_codes: codes }
end
+
+ post "/notify_post_receive" do
+ status 200
+
+ return unless Gitlab::GitalyClient.enabled?
+
+ begin
+ Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path])
+ rescue GRPC::Unavailable => e
+ render_api_error(e, 500)
+ end
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 6d30c5d81b1..fd2674910d2 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -25,6 +25,7 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
+ optional :iids, type: Array[Integer], desc: 'The IID array of issues'
use :pagination
end
@@ -40,7 +41,7 @@ module API
resource :issues do
desc "Get currently authenticated user's issues" do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -50,16 +51,16 @@ module API
get do
issues = find_issues(scope: 'authored')
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'opened',
@@ -71,18 +72,18 @@ module API
issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -90,21 +91,21 @@ module API
use :issues_params
end
get ":id/issues" do
- project = find_project(params[:id])
+ project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
- get ":id/issues/:issue_id" do
- issue = find_project_issue(params[:issue_id])
+ get ":id/issues/:issue_iid" do
+ issue = find_project_issue(params[:issue_iid])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
@@ -115,8 +116,10 @@ module API
requires :title, type: String, desc: 'The title of an issue'
optional :created_at, type: DateTime,
desc: 'Date time when the issue was created. Available only for admins and project owners.'
- optional :merge_request_for_resolving_discussions, type: Integer,
+ optional :merge_request_to_resolve_discussions_of, type: Integer,
desc: 'The IID of a merge request for which to resolve discussions'
+ optional :discussion_to_resolve, type: String,
+ desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
use :issue_params
end
post ':id/issues' do
@@ -127,12 +130,6 @@ module API
issue_params = declared_params(include_missing: false)
- if merge_request_iid = params[:merge_request_for_resolving_discussions]
- issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
-
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
@@ -151,7 +148,7 @@ module API
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
optional :title, type: String, desc: 'The title of an issue'
optional :updated_at, type: DateTime,
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
@@ -160,8 +157,8 @@ module API
at_least_one_of :title, :description, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event
end
- put ':id/issues/:issue_id' do
- issue = user_project.issues.find(params.delete(:issue_id))
+ put ':id/issues/:issue_iid' do
+ issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
# Setting created_at time only allowed for admins and project owners
@@ -188,11 +185,11 @@ module API
success Entities::Issue
end
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
- post ':id/issues/:issue_id/move' do
- issue = user_project.issues.find_by(id: params[:issue_id])
+ post ':id/issues/:issue_iid/move' do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
new_project = Project.find_by(id: params[:to_project_id])
@@ -208,10 +205,10 @@ module API
desc 'Delete a project issue'
params do
- requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
- delete ":id/issues/:issue_id" do
- issue = user_project.issues.find_by(id: params[:issue_id])
+ delete ":id/issues/:issue_iid" do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
new file mode 100644
index 00000000000..ffab0aafe59
--- /dev/null
+++ b/lib/api/jobs.rb
@@ -0,0 +1,252 @@
+module API
+ class Jobs < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: ::CommitStatus::AVAILABLE_STATUSES,
+ coerce_with: ->(scope) {
+ case scope
+ when String
+ [scope]
+ when Hashie::Mash
+ scope.values
+ when Hashie::Array
+ scope
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a projects jobs' do
+ success Entities::Job
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/jobs' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Job
+ end
+
+ desc 'Get pipeline jobs' do
+ success Entities::Job
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/pipelines/:pipeline_id/jobs' do
+ pipeline = user_project.pipelines.find(params[:pipeline_id])
+ builds = pipeline.builds
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Job
+ end
+
+ desc 'Get a specific job of a project' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id' do
+ authorize_read_builds!
+
+ build = get_build!(params[:job_id])
+
+ present build, with: Entities::Job
+ end
+
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id/artifacts' do
+ authorize_read_builds!
+
+ build = get_build!(params[:job_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ end
+ get ':id/jobs/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific job of a project'
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id/trace' do
+ authorize_read_builds!
+
+ build = get_build!(params[:job_id])
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ desc 'Cancel a specific job of a project' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build!(params[:job_id])
+
+ build.cancel
+
+ present build, with: Entities::Job
+ end
+
+ desc 'Retry a specific build of a project' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/jobs/:job_id/retry' do
+ authorize_update_builds!
+
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build, current_user)
+
+ present build, with: Entities::Job
+ end
+
+ desc 'Erase job (remove artifacts and the trace)' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/jobs/:job_id/erase' do
+ authorize_update_builds!
+
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: Entities::Job
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build!(params[:job_id])
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Job
+ end
+
+ desc 'Trigger a manual job' do
+ success Entities::Job
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a Job'
+ end
+ post ":id/jobs/:job_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:job_id])
+
+ bad_request!("Unplayable Job") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: Entities::Job
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index d2955af3f95..d9a3cb7bb6b 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,13 +1,13 @@
module API
class Labels < Grape::API
include PaginationParams
-
+
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success Entities::Label
end
@@ -56,7 +56,7 @@ module API
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
- present label.destroy, with: Entities::Label, current_user: current_user, project: user_project
+ label.destroy
end
desc 'Update an existing label. At least one optional parameter is required.' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d1d78775c6d..c200e46a328 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
@@ -55,7 +55,6 @@ module API
authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id])
-
conflict!('Member already exists') if member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
@@ -63,9 +62,6 @@ module API
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
@@ -79,18 +75,14 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
put ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
+ source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params[:user_id])
- attrs = attributes_for_keys [:access_level, :expires_at]
+ member = source.members.find_by!(user_id: params.delete(:user_id))
- if member.update_attributes(attrs)
+ if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member
else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
@@ -101,24 +93,10 @@ module API
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
+ # Ensure that memeber exists
+ source.members.find_by!(user_id: params[:user_id])
- # This is to ensure back-compatibility but find_by! should be used
- # in that casse in 9.0!
- member = source.members.find_by(user_id: params[:user_id])
-
- # This is to ensure back-compatibility but this should be removed in
- # favor of find_by! in 9.0!
- not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
-
- # This is to ensure back-compatibility but 204 behavior should be used
- # for all DELETE endpoints in 9.0!
- if member.nil?
- { message: "Access revoked", id: params[:user_id].to_i }
- else
- ::Members::DestroyService.new(source, current_user, declared_params).execute
-
- present member.user, with: Entities::Member, member: member
- end
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
end
end
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 4901a7cfea6..4b79eac2b8b 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -5,19 +5,21 @@ module API
before { authenticate! }
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::MergeRequestDiff
end
params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
use :pagination
end
- get ":id/merge_requests/:merge_request_id/versions" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ":id/merge_requests/:merge_request_iid/versions" do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
end
@@ -28,13 +30,12 @@ module API
end
params do
- requires :id, type: String, desc: 'The ID of a project'
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
- get ":id/merge_requests/:merge_request_id/versions/:version_id" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bdd764abfeb..5cc807d5bff 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
helpers do
@@ -25,6 +25,14 @@ module API
render_api_error!(errors, 400)
end
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ Entities::ExternalIssue
+ else
+ Entities::IssueBasic
+ end
+ end
+
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -35,7 +43,7 @@ module API
end
desc 'List merge requests' do
- success Entities::MergeRequest
+ success Entities::MergeRequestBasic
end
params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -62,7 +70,7 @@ module API
end
merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end
desc 'Create a merge request' do
@@ -93,23 +101,23 @@ module API
desc 'Delete a merge request'
params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
- delete ":id/merge_requests/:merge_request_id" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ delete ":id/merge_requests/:merge_request_iid" do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
end
params do
- requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
desc 'Get a single merge request' do
success Entities::MergeRequest
end
- get ':id/merge_requests/:merge_request_id' do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -117,8 +125,8 @@ module API
desc 'Get the commits of a merge request' do
success Entities::RepoCommit
end
- get ':id/merge_requests/:merge_request_id/commits' do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ':id/merge_requests/:merge_request_iid/commits' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits = ::Kaminari.paginate_array(merge_request.commits)
present paginate(commits), with: Entities::RepoCommit
@@ -127,8 +135,8 @@ module API
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
- get ':id/merge_requests/:merge_request_id/changes' do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ':id/merge_requests/:merge_request_iid/changes' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
@@ -146,8 +154,8 @@ module API
:milestone_id, :labels, :state_event,
:remove_source_branch
end
- put ':id/merge_requests/:merge_request_id' do
- merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+ put ':id/merge_requests/:merge_request_iid' do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
@@ -168,12 +176,12 @@ module API
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
- optional :merge_when_build_succeeds, type: Boolean,
- desc: 'When true, this merge request will be merged when the pipeline succeeds'
+ optional :merge_when_pipeline_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the pipeline succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
- put ':id/merge_requests/:merge_request_id/merge' do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ put ':id/merge_requests/:merge_request_iid/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
# Merge request can not be merged
# because user dont have permissions to push into target branch
@@ -192,7 +200,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params)
.execute(merge_request)
@@ -208,10 +216,10 @@ module API
desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
success Entities::MergeRequest
end
- post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
::MergeRequest::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
@@ -224,8 +232,8 @@ module API
params do
use :pagination
end
- get ':id/merge_requests/:merge_request_id/comments' do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ':id/merge_requests/:merge_request_iid/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
@@ -235,8 +243,8 @@ module API
params do
requires :note, type: String, desc: 'The text of the comment'
end
- post ':id/merge_requests/:merge_request_id/comments' do
- merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+ post ':id/merge_requests/:merge_request_iid/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note)
opts = {
note: params[:note],
@@ -259,8 +267,8 @@ module API
params do
use :pagination
end
- get ':id/merge_requests/:merge_request_id/closes_issues' do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ get ':id/merge_requests/:merge_request_iid/closes_issues' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 0b4ed76b35c..e7ab82f08db 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -23,14 +23,15 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of project milestones' do
success Entities::Milestone
end
params do
optional :state, type: String, values: %w[active closed all], default: 'all',
desc: 'Return "active", "closed", or "all" milestones'
- optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+ optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+ optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
use :pagination
end
get ":id/milestones" do
@@ -38,7 +39,8 @@ module API
milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
- milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+ milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
+ milestones = filter_by_search(milestones, params[:search]) if params[:search]
present paginate(milestones), with: Entities::Milestone
end
@@ -101,7 +103,7 @@ module API
end
desc 'Get all issues for a single project milestone' do
- success Entities::Issue
+ success Entities::IssueBasic
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -114,16 +116,17 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_title: milestone.title
+ milestone_title: milestone.title,
+ sort: 'position_asc'
}
issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
+ present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
end
desc 'Get all merge requests for a single project milestone' do
detail 'This feature was introduced in GitLab 9.'
- success Entities::MergeRequest
+ success Entities::MergeRequestBasic
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -136,11 +139,15 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_id: milestone.id
+ milestone_id: milestone.id,
+ sort: 'position_asc'
}
merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests),
+ with: Entities::MergeRequestBasic,
+ current_user: current_user,
+ project: user_project
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8beccaaabd1..29ceffdbd2d 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -4,12 +4,12 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
@@ -85,7 +85,7 @@ module API
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid?
- present note, with: Entities::const_get(note.class.name)
+ present note, with: Entities.const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
@@ -132,8 +132,6 @@ module API
authorize! :admin_note, note
::Notes::DestroyService.new(user_project, current_user).execute(note)
-
- present note, with: Entities::Note
end
end
end
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index c5e9b3ad69b..992ea5dc24d 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -48,14 +48,14 @@ module API
end
%w[group project].each do |source_type|
- resource source_type.pluralize do
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
end
- params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
- end
get ":id/notification_settings" do
source = find_source(source_type, params[:id])
@@ -69,7 +69,6 @@ module API
success Entities::NotificationSetting
end
params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
optional :level, type: String, desc: "The #{source_type} notification level"
NotificationSetting::EMAIL_EVENTS.each do |event|
optional event, type: Boolean, desc: 'Enable/disable this notification'
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index f59f7959173..754c3d85a04 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -7,21 +7,21 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
+ success Entities::PipelineBasic
end
params do
use :pagination
- optional :scope, type: String, values: ['running', 'branches', 'tags'],
+ optional :scope, type: String, values: %w(running branches tags),
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
- present paginate(pipelines), with: Entities::Pipeline
+ present paginate(pipelines), with: Entities::PipelineBasic
end
desc 'Create a new pipeline' do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index f7a28d7ad10..53791166c33 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -24,7 +24,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project hooks' do
success Entities::ProjectHook
end
@@ -90,12 +90,9 @@ module API
requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
end
delete ":id/hooks/:hook_id" do
- begin
- present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook
- rescue
- # ProjectHook can raise Error if hook_id not found
- not_found!("Error deleting hook #{params[:hook_id]}")
- end
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ hook.destroy
end
end
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 2a1cce73f3f..cfee38a9baf 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -50,11 +50,9 @@ module API
requires :title, type: String, desc: 'The title of the snippet'
requires :file_name, type: String, desc: 'The file name of the snippet'
requires :code, type: String, desc: 'The content of the snippet'
- requires :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ requires :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
@@ -80,11 +78,9 @@ module API
optional :title, type: String, desc: 'The title of the snippet'
optional :file_name, type: String, desc: 'The file name of the snippet'
optional :code, type: String, desc: 'The content of the snippet'
- optional :visibility_level, type: Integer,
- values: [Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC],
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 366e5679edd..0fbe1669d45 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,13 +16,10 @@ module API
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :visibility_level, type: Integer, values: [
- Gitlab::VisibilityLevel::PRIVATE,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
optional :public_builds, type: Boolean, desc: 'Perform public builds'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
- optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
end
end
@@ -47,11 +44,12 @@ module API
params :filter_params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
- optional :visibility, type: String, values: %w[public internal private],
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
- optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :search, type: String, desc: 'Return list of projects matching the search criteria'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
end
params :statistics_params do
@@ -93,8 +91,9 @@ module API
success Entities::Project
end
params do
- requires :name, type: String, desc: 'The name of the project'
+ optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
use :optional_params
use :create_params
end
@@ -143,7 +142,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: /[^\/]+/ } do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
@@ -206,8 +205,8 @@ module API
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
:shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :visibility_level, :public_builds,
- :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :lfs_enabled, :visibility, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_pipeline_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
end
@@ -215,7 +214,7 @@ module API
authorize_admin_project
attrs = declared_params(include_missing: false)
authorize! :rename_project, user_project if attrs[:name].present?
- authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+ authorize! :change_visibility_level, user_project if attrs[:visibility].present?
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
@@ -281,6 +280,8 @@ module API
delete ":id" do
authorize! :remove_project, user_project
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+
+ accepted!
end
desc 'Mark this project as forked from another'
@@ -350,7 +351,6 @@ module API
not_found!('Group Link') unless link
link.destroy
- no_content!
end
desc 'Upload a file'
@@ -374,6 +374,19 @@ module API
present paginate(users), with: Entities::UserBasic
end
+
+ desc 'Start the housekeeping task for a project' do
+ detail 'This feature was introduced in GitLab 9.0.'
+ end
+ post ':id/housekeeping' do
+ authorize_admin_project
+
+ begin
+ ::Projects::HousekeepingService.new(user_project).execute
+ rescue ::Projects::HousekeepingService::LeaseTaken => error
+ conflict!(error.message)
+ end
+ end
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index bfda6f45b0a..8f16e532ecb 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -17,19 +17,34 @@ module API
end
not_found!
end
+
+ def assign_blob_vars!
+ authorize! :download_code, user_project
+
+ @repo = user_project.repository
+
+ begin
+ @blob = Gitlab::Git::Blob.raw(@repo, params[:sha])
+ @blob.load_all_data!(@repo)
+ rescue
+ not_found! 'Blob'
+ end
+
+ not_found! 'Blob' unless @blob
+ end
end
desc 'Get a project repository tree' do
success Entities::RepoTreeObject
end
params do
- optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :path, type: String, desc: 'The path of the tree'
optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
use :pagination
end
get ':id/repository/tree' do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ ref = params[:ref] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
commit = user_project.commit(ref)
@@ -40,39 +55,29 @@ module API
present paginate(entries), with: Entities::RepoTreeObject
end
- desc 'Get a raw file contents'
+ desc 'Get raw blob contents from the repository'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
- requires :filepath, type: String, desc: 'The path to the file to display'
end
- get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do
- repo = user_project.repository
-
- commit = repo.commit(params[:sha])
- not_found! "Commit" unless commit
+ get ':id/repository/blobs/:sha/raw' do
+ assign_blob_vars!
- blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
- not_found! "File" unless blob
-
- send_git_blob repo, blob
+ send_git_blob @repo, @blob
end
- desc 'Get a raw blob contents by blob sha'
+ desc 'Get a blob from the repository'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
end
- get ':id/repository/raw_blobs/:sha' do
- repo = user_project.repository
-
- begin
- blob = Gitlab::Git::Blob.raw(repo, params[:sha])
- rescue
- not_found! 'Blob'
- end
-
- not_found! 'Blob' unless blob
+ get ':id/repository/blobs/:sha' do
+ assign_blob_vars!
- send_git_blob repo, blob
+ {
+ size: @blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(@blob.data),
+ sha: @blob.id
+ }
end
desc 'Get an archive of the repository'
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 00000000000..4c9db2c8716
--- /dev/null
+++ b/lib/api/runner.rb
@@ -0,0 +1,264 @@
+module API
+ class Runner < Grape::API
+ helpers ::API::Helpers::Runner
+
+ resource :runners do
+ desc 'Registers a new Runner' do
+ success Entities::RunnerRegistrationDetails
+ http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: 'Registration token'
+ optional :description, type: String, desc: %q(Runner's description)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+ optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
+ optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+ end
+ post '/' do
+ attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+
+ runner =
+ if runner_registration_token_valid?
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(attributes.merge(is_shared: true))
+ elsif project = Project.find_by(runners_token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(attributes)
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ runner.update(get_runner_version_from_params)
+ present runner, with: Entities::RunnerRegistrationDetails
+ else
+ not_found!
+ end
+ end
+
+ desc 'Deletes a registered Runner' do
+ http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ delete '/' do
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+
+ desc 'Validates authentication credentials' do
+ http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ post '/verify' do
+ authenticate_runner!
+ status 200
+ end
+ end
+
+ resource :jobs do
+ desc 'Request a job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Job was scheduled'],
+ [204, 'No job for Runner'],
+ [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ optional :last_update, type: String, desc: %q(Runner's queue last_update token)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ end
+ post '/request' do
+ authenticate_runner!
+ no_content! unless current_runner.active?
+ update_runner_info
+
+ if current_runner.is_runner_queue_value_latest?(params[:last_update])
+ header 'X-GitLab-Last-Update', params[:last_update]
+ Gitlab::Metrics.add_event(:build_not_found_cached)
+ return no_content!
+ end
+
+ new_update = current_runner.ensure_runner_queue_value
+ result = ::Ci::RegisterJobService.new(current_runner).execute
+
+ if result.valid?
+ if result.build
+ Gitlab::Metrics.add_event(:build_found,
+ project: result.build.project.path_with_namespace)
+ present result.build, with: Entities::JobRequest::Response
+ else
+ Gitlab::Metrics.add_event(:build_not_found)
+ header 'X-GitLab-Last-Update', new_update
+ no_content!
+ end
+ else
+ # We received build that is invalid due to concurrency conflict
+ Gitlab::Metrics.add_event(:build_invalid)
+ conflict!
+ end
+ end
+
+ desc 'Updates a job' do
+ http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runners's authentication token)
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :trace, type: String, desc: %q(Job's full trace)
+ optional :state, type: String, desc: %q(Job's status: success, failed)
+ end
+ put '/:id' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ job.update_attributes(trace: params[:trace]) if params[:trace]
+
+ Gitlab::Metrics.add_event(:update_build,
+ project: job.project.path_with_namespace)
+
+ case params[:state].to_s
+ when 'success'
+ job.success
+ when 'failed'
+ job.drop
+ end
+ end
+
+ desc 'Appends a patch to the job trace' do
+ http_codes [[202, 'Trace was patched'],
+ [400, 'Missing Content-Range header'],
+ [403, 'Forbidden'],
+ [416, 'Range not satisfiable']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ end
+ patch '/:id/trace' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ content_range = request.headers['Content-Range']
+ content_range = content_range.split('-')
+
+ current_length = job.trace_length
+ unless current_length == content_range[0].to_i
+ return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" })
+ end
+
+ job.append_trace(request.body.read, content_range[0].to_i)
+
+ status 202
+ header 'Job-Status', job.status
+ header 'Range', "0-#{job.trace_length}"
+ end
+
+ desc 'Authorize artifacts uploading for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :filesize, type: Integer, desc: %q(Artifacts filesize)
+ end
+ post '/:id/artifacts/authorize' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+ Gitlab::Workhorse.verify_api_request!(headers)
+
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+ forbidden!('Job is not running') unless job.running?
+
+ if params[:filesize]
+ file_size = params[:filesize].to_i
+ file_to_large! unless file_size < max_artifacts_size
+ end
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ Gitlab::Workhorse.artifact_upload_ok
+ end
+
+ desc 'Upload artifacts for job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Artifact uploaded'],
+ [400, 'Bad request'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :expire_in, type: String, desc: %q(Specify when artifacts should expire)
+ optional :file, type: File, desc: %q(Artifact's file)
+ optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+ optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
+ optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
+ optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
+ optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
+ end
+ post '/:id/artifacts' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+ forbidden!('Job is not running!') unless job.running?
+
+ artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+ artifacts = uploaded_file(:file, artifacts_upload_path)
+ metadata = uploaded_file(:metadata, artifacts_upload_path)
+
+ bad_request!('Missing artifacts file!') unless artifacts
+ file_to_large! unless artifacts.size < max_artifacts_size
+
+ job.artifacts_file = artifacts
+ job.artifacts_metadata = metadata
+ job.artifacts_expire_in = params['expire_in'] ||
+ Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
+
+ if job.save
+ present job, with: Entities::JobRequest::Response
+ else
+ render_validation_error!(job)
+ end
+ end
+
+ desc 'Download the artifacts file for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [404, 'Artifact not found']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ end
+ get '/:id/artifacts' do
+ job = Ci::Build.find_by_id(params[:id])
+ authenticate_job!(job)
+
+ artifacts_file = job.artifacts_file
+ unless artifacts_file.file_storage?
+ return redirect_to job.artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ not_found!
+ end
+
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ end
+ end
+ end
+end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4fbd4096533..a77c876a749 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
- runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
+ runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner
end
@@ -78,16 +78,15 @@ module API
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
- runner.destroy!
- present runner, with: Entities::Runner
+ runner.destroy!
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc 'Get runners available for project' do
@@ -136,8 +135,6 @@ module API
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
runner_project.destroy
-
- present runner, with: Entities::Runner
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1456fe4688b..4e0c9cb1f63 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
desc: 'Enable SSL verification for communication'
}
],
- 'builds-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :add_pusher,
- type: Boolean,
- desc: 'Add pusher to recipients list'
- },
- {
- required: false,
- name: :notify_only_broken_builds,
- type: Boolean,
- desc: 'Notify only broken builds'
- }
- ],
'campfire' => [
{
required: true,
@@ -403,9 +383,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_builds,
+ name: :notify_only_broken_pipelines,
type: Boolean,
- desc: 'Notify only broken builds'
+ desc: 'Notify only broken pipelines'
}
],
'pivotaltracker' => [
@@ -422,6 +402,14 @@ module API
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
],
+ 'prometheus' => [
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ],
'pushover' => [
{
required: true,
@@ -542,7 +530,6 @@ module API
BambooService,
BugzillaService,
BuildkiteService,
- BuildsEmailService,
CampfireService,
CustomIssueTrackerService,
DroneCiService,
@@ -558,12 +545,26 @@ module API
SlackSlashCommandsService,
PipelinesEmailService,
PivotaltrackerService,
+ PrometheusService,
PushoverService,
RedmineService,
SlackService,
MattermostService,
TeamcityService,
- ].freeze
+ ]
+
+ if Rails.env.development?
+ services['mock-ci'] = [
+ {
+ required: true,
+ name: :mock_service_url,
+ type: String,
+ desc: 'URL to the mock service'
+ }
+ ]
+
+ service_classes << MockCiService
+ end
trigger_services = {
'mattermost-slash-commands' => [
@@ -582,7 +583,10 @@ module API
]
}.freeze
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authenticate! }
before { authorize_admin_project }
@@ -598,7 +602,7 @@ module API
desc "Set #{service_slug} service for project"
params do
service_classes.each do |service|
- event_names = service.try(:event_names) || []
+ event_names = service.try(:event_names) || next
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
@@ -641,9 +645,7 @@ module API
hash.merge!(key => nil)
end
- if service.update_attributes(attrs.merge(active: false))
- true
- else
+ unless service.update_attributes(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
@@ -672,7 +674,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 747ceb4e3e0..d4d3229f0d1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -21,9 +21,9 @@ module API
end
params do
optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
- optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
- optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
- optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
@@ -56,7 +56,8 @@ module API
given shared_runners_enabled: ->(val) { val } do
requires :shared_runners_text, type: String, desc: 'Shared runners text '
end
- optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
+ optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
@@ -117,7 +118,9 @@ module API
:send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
:after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
:home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+ :shared_runners_enabled, :max_artifacts_size,
+ :default_artifacts_expire_in, :max_pages_size,
+ :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
@@ -125,7 +128,9 @@ module API
:housekeeping_enabled, :terminal_max_session_time
end
put "application/settings" do
- if current_settings.update_attributes(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
+
+ if current_settings.update_attributes(attrs)
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index ac03fbd2a3d..b93fdc62808 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -58,10 +58,10 @@ module API
requires :title, type: String, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
requires :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- default: Gitlab::VisibilityLevel::INTERNAL,
- desc: 'The visibility level of the snippet'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ default: 'internal',
+ desc: 'The visibility of the snippet'
end
post do
attrs = declared_params(include_missing: false).merge(request: request, api: true)
@@ -85,10 +85,10 @@ module API
optional :title, type: String, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, desc: 'The content of a snippet'
- optional :visibility_level, type: Integer,
- values: Gitlab::VisibilityLevel.values,
- desc: 'The visibility level of the snippet'
- at_least_one_of :title, :file_name, :content, :visibility_level
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ desc: 'The visibility of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
@@ -118,9 +118,10 @@ module API
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :destroy_personal_snippet, snippet
+
snippet.destroy
- no_content!
end
desc 'Get a raw snippet' do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index acf11dbdf26..dbe54d3cd31 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,7 +3,6 @@ module API
before { authenticate! }
subscribable_types = {
- 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) },
@@ -13,7 +12,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index d038a3fa828..ed7b23b474a 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -66,7 +66,7 @@ module API
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
- present hook.destroy, with: Entities::Hook
+ hook.destroy
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 86759ab882f..c7b1efe0bfa 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success Entities::RepoTag
end
@@ -66,11 +66,7 @@ module API
result = ::Tags::DestroyService.new(user_project, current_user).
execute(params[:tag_name])
- if result[:status] == :success
- {
- tag_name: params[:tag_name]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index 85b5f7d98b8..05b4b490e27 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -5,11 +5,11 @@ module API
included do
helpers do
def issuable_name
- declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request'
end
def issuable_key
- "#{issuable_name}_id".to_sym
+ "#{issuable_name}_iid".to_sym
end
def update_issuable_key
@@ -50,7 +50,7 @@ module API
issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
issuable_collection_name = issuable_name.pluralize
- issuable_key = "#{issuable_name}_id".to_sym
+ issuable_key = "#{issuable_name}_iid".to_sym
desc "Set a time estimate for a project #{issuable_name}"
params do
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 0b9650b296c..d1f7e364029 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,22 +5,22 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
- 'merge_requests' => ->(id) { find_merge_request_with_access(id) },
- 'issues' => ->(id) { find_project_issue(id) }
- }
+ 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
+ 'issues' => ->(iid) { find_project_issue(iid) }
+ }.freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
ISSUABLE_TYPES.each do |type, finder|
- type_id_str = "#{type.singularize}_id".to_sym
+ type_id_str = "#{type.singularize}_iid".to_sym
desc 'Create a todo on an issuable' do
success Entities::Todo
end
params do
- requires type_id_str, type: Integer, desc: 'The ID of an issuable'
+ requires type_id_str, type: Integer, desc: 'The IID of an issuable'
end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 87a717ba751..a9f2ca2608e 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -5,38 +5,33 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
- desc 'Trigger a GitLab project build' do
- success Entities::TriggerRequest
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Trigger a GitLab project pipeline' do
+ success Entities::Pipeline
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/builds" do
+ post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
# validate variables
- variables = params[:variables]
- if variables
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # convert variables from Mash to Hash
- variables = variables.to_h
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
if trigger_request
- present trigger_request, with: Entities::TriggerRequest
+ present trigger_request.pipeline, with: Entities::Pipeline
else
- errors = 'No builds created'
+ errors = 'No pipeline created'
render_api_error!(errors, 400)
end
end
@@ -60,13 +55,13 @@ module API
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- get ':id/triggers/:token' do
+ get ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
present trigger, with: Entities::Trigger
@@ -75,31 +70,79 @@ module API
desc 'Create a trigger' do
success Entities::Trigger
end
+ params do
+ requires :description, type: String, desc: 'The trigger description'
+ end
post ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.create
+ trigger = user_project.triggers.create(
+ declared_params(include_missing: false).merge(owner: current_user))
- present trigger, with: Entities::Trigger
+ if trigger.valid?
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Update a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ optional :description, type: String, desc: 'The trigger description'
+ end
+ put ':id/triggers/:trigger_id' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(declared_params(include_missing: false))
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
+ end
+
+ desc 'Take ownership of trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
+ end
+ post ':id/triggers/:trigger_id/take_ownership' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
+ return not_found!('Trigger') unless trigger
+
+ if trigger.update(owner: current_user)
+ status :ok
+ present trigger, with: Entities::Trigger
+ else
+ render_validation_error!(trigger)
+ end
end
desc 'Delete a trigger' do
success Entities::Trigger
end
params do
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :trigger_id, type: Integer, desc: 'The trigger ID'
end
- delete ':id/triggers/:token' do
+ delete ':id/triggers/:trigger_id' do
authenticate!
authorize! :admin_build, user_project
- trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
trigger.destroy
-
- present trigger, with: Entities::Trigger
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index fbc17953691..2d4d5a25221 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -9,6 +9,11 @@ module API
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
+ def find_user(params)
+ id = params[:user_id] || params[:id]
+ User.find_by(id: id) || not_found!('User')
+ end
+
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
@@ -40,7 +45,7 @@ module API
use :pagination
end
get do
- unless can?(current_user, :read_users_list, nil)
+ unless can?(current_user, :read_users_list)
render_api_error!("Not authorized.", 403)
end
@@ -172,7 +177,7 @@ module API
end
end
- user_params.merge!(password_expires_at: Time.now) if user_params[:password].present?
+ user_params[:password_expires_at] = Time.now if user_params[:password].present?
if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
@@ -236,7 +241,7 @@ module API
key = user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc 'Add an email address to a specified user. Available only for admins.' do
@@ -362,6 +367,76 @@ module API
present paginate(events), with: Entities::Event
end
+
+ params do
+ requires :user_id, type: Integer, desc: 'The ID of the user'
+ end
+ segment ':user_id' do
+ resource :impersonation_tokens do
+ helpers do
+ def finder(options = {})
+ user = find_user(params)
+ PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
+ end
+
+ def find_impersonation_token
+ finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
+ end
+ end
+
+ before { authenticated_as_admin! }
+
+ desc 'Retrieve impersonation tokens. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ use :pagination
+ optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens'
+ end
+ get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken }
+
+ desc 'Create a impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the impersonation token'
+ optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token'
+ optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token'
+ end
+ post do
+ impersonation_token = finder.build(declared_params(include_missing: false))
+
+ if impersonation_token.save
+ present impersonation_token, with: Entities::ImpersonationToken
+ else
+ render_validation_error!(impersonation_token)
+ end
+ end
+
+ desc 'Retrieve impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ success Entities::ImpersonationToken
+ end
+ params do
+ requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+ end
+ get ':impersonation_token_id' do
+ present find_impersonation_token, with: Entities::ImpersonationToken
+ end
+
+ desc 'Revoke a impersonation token. Available only for admins.' do
+ detail 'This feature was introduced in GitLab 9.0'
+ end
+ params do
+ requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
+ end
+ delete ':impersonation_token_id' do
+ find_impersonation_token.revoke!
+ end
+ end
+ end
end
resource :user do
@@ -422,7 +497,7 @@ module API
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc "Get the currently authenticated user's email addresses" do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
new file mode 100644
index 00000000000..b96b2d70b12
--- /dev/null
+++ b/lib/api/v3/award_emoji.rb
@@ -0,0 +1,130 @@
+module API
+ module V3
+ class AwardEmoji < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ AWARDABLES = %w[issue merge_request snippet].freeze
+
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ AWARDABLES.each do |awardable_type|
+ awardable_string = awardable_type.pluralize
+ awardable_id_string = "#{awardable_type}_id"
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
+ end
+
+ [
+ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
+ ].each do |endpoint|
+
+ desc 'Get a list of project +awardable+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ use :pagination
+ end
+ get endpoint do
+ if can_read_awardable?
+ awards = awardable.award_emoji
+ present paginate(awards), with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ desc 'Get a specific award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of the award'
+ end
+ get "#{endpoint}/:award_id" do
+ if can_read_awardable?
+ present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ desc 'Award a new Emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
+ end
+ post endpoint do
+ not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
+
+ award = awardable.create_award_emoji(params[:name], current_user)
+
+ if award.persisted?
+ present award, with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji #{award.errors.messages}")
+ end
+ end
+
+ desc 'Delete a +awardables+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of an award emoji'
+ end
+ delete "#{endpoint}/:award_id" do
+ award = awardable.award_emoji.find(params[:award_id])
+
+ unauthorized! unless award.user == current_user || current_user.admin?
+
+ award.destroy
+ present award, with: Entities::AwardEmoji
+ end
+ end
+ end
+ end
+
+ helpers do
+ def can_read_awardable?
+ can?(current_user, read_ability(awardable), awardable)
+ end
+
+ def can_award_awardable?
+ awardable.user_can_award?(current_user, params[:name])
+ end
+
+ def awardable
+ @awardable ||=
+ begin
+ if params.include?(:note_id)
+ note_id = params.delete(:note_id)
+
+ awardable.notes.find(note_id)
+ elsif params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ elsif params.include?(:merge_request_id)
+ user_project.merge_requests.find(params[:merge_request_id])
+ else
+ user_project.snippets.find(params[:snippet_id])
+ end
+ end
+ end
+
+ def read_ability(awardable)
+ case awardable
+ when Note
+ read_ability(awardable.noteable)
+ else
+ :"read_#{awardable.class.to_s.underscore}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
index 31d708bc2c8..94acc67171e 100644
--- a/lib/api/v3/boards.rb
+++ b/lib/api/v3/boards.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success ::API::Entities::Board
@@ -44,6 +44,27 @@ module API
authorize!(:read_board, user_project)
present board_lists, with: ::API::Entities::List
end
+
+ desc 'Delete a board list' do
+ detail 'This feature was introduced in 8.13'
+ success ::API::Entities::List
+ end
+ params do
+ requires :list_id, type: Integer, desc: 'The ID of a board list'
+ end
+ delete "/lists/:list_id" do
+ authorize!(:admin_list, user_project)
+
+ list = board_lists.find(params[:list_id])
+
+ service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+
+ if service.execute(list)
+ present list, with: ::API::Entities::List
+ else
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
+ end
end
end
end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 733c6b21be5..0a877b960f6 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success ::API::Entities::RepoBranch
end
@@ -18,6 +18,54 @@ module API
present branches, with: ::API::Entities::RepoBranch, project: user_project
end
+
+ desc 'Delete a branch'
+ params do
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
+ authorize_push_project
+
+ result = DeleteBranchService.new(user_project, current_user).
+ execute(params[:branch])
+
+ if result[:status] == :success
+ status(200)
+ {
+ branch_name: params[:branch]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
+
+ desc 'Delete all merged branches'
+ delete ":id/repository/merged_branches" do
+ DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+ status(200)
+ end
+
+ desc 'Create branch' do
+ success ::API::Entities::RepoBranch
+ end
+ params do
+ requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
+ end
+ post ":id/repository/branches" do
+ authorize_push_project
+ result = CreateBranchService.new(user_project, current_user).
+ execute(params[:branch_name], params[:ref])
+
+ if result[:status] == :success
+ present result[:branch],
+ with: ::API::Entities::RepoBranch,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
end
end
end
diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb
new file mode 100644
index 00000000000..417e4ad0b26
--- /dev/null
+++ b/lib/api/v3/broadcast_messages.rb
@@ -0,0 +1,31 @@
+module API
+ module V3
+ class BroadcastMessages < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authenticated_as_admin! }
+
+ resource :broadcast_messages do
+ helpers do
+ def find_message
+ BroadcastMessage.find(params[:id])
+ end
+ end
+
+ desc 'Delete a broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::BroadcastMessage
+ end
+ params do
+ requires :id, type: Integer, desc: 'Broadcast message ID'
+ end
+ delete ':id' do
+ message = find_message
+
+ present message.destroy, with: ::API::Entities::BroadcastMessage
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
new file mode 100644
index 00000000000..6f97102c6ef
--- /dev/null
+++ b/lib/api/v3/builds.rb
@@ -0,0 +1,255 @@
+module API
+ module V3
+ class Builds < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: %w(pending running failed success canceled skipped),
+ coerce_with: ->(scope) {
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a project builds' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build
+ end
+
+ desc 'Get builds for a specific commit of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :sha, type: String, desc: 'The SHA id of a commit'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/builds' do
+ authorize_read_builds!
+
+ return not_found! unless user_project.commit(params[:sha])
+
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: ::API::V3::Entities::Build
+ end
+
+ desc 'Get a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/artifacts' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the build'
+ end
+ get ':id/builds/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific build of a project'
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/trace' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ desc 'Cancel a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+
+ build.cancel
+
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Retry a specific build of a project' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/retry' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build, current_user)
+
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Erase build (remove artifacts and build trace)' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/erase' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success ::API::V3::Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: ::API::V3::Entities::Build
+ end
+
+ desc 'Trigger a manual build' do
+ success ::API::V3::Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Job") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: ::API::V3::Entities::Build
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 477e22fd25e..3414a2883e5 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
success ::API::Entities::RepoCommit
end
@@ -55,13 +55,6 @@ module API
branch = attrs.delete(:branch_name)
attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
- attrs[:actions].map! do |action|
- action[:action] = action[:action].to_sym
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
- action
- end
-
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success
@@ -137,9 +130,7 @@ module API
commit_params = {
commit: commit,
- create_merge_request: false,
- source_project: user_project,
- source_branch: commit.cherry_pick_branch_name,
+ start_branch: params[:branch],
target_branch: params[:branch]
}
@@ -162,7 +153,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
- requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+ requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments' do
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
index 5bbb167755c..bbb174b6003 100644
--- a/lib/api/v3/deploy_keys.rb
+++ b/lib/api/v3/deploy_keys.rb
@@ -13,7 +13,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
%w(keys deploy_keys).each do |path|
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 00000000000..1d4972eda26
--- /dev/null
+++ b/lib/api/v3/deployments.rb
@@ -0,0 +1,43 @@
+module API
+ module V3
+ # Deployments RESTful API endpoints
+ class Deployments < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get all deployments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success ::API::V3::Deployments
+ end
+ params do
+ use :pagination
+ end
+ get ':id/deployments' do
+ authorize! :read_deployment, user_project
+
+ present paginate(user_project.deployments), with: ::API::V3::Deployments
+ end
+
+ desc 'Gets a specific deployment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success ::API::V3::Deployments
+ end
+ params do
+ requires :deployment_id, type: Integer, desc: 'The deployment ID'
+ end
+ get ':id/deployments/:deployment_id' do
+ authorize! :read_deployment, user_project
+
+ deployment = user_project.deployments.find(params[:deployment_id])
+
+ present deployment, with: ::API::V3::Deployments
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3cc0dc968a8..832b4bdeb4f 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -11,6 +11,243 @@ module API
Gitlab::UrlBuilder.build(snippet)
end
end
+
+ class Note < Grape::Entity
+ expose :id
+ expose :note, as: :body
+ expose :attachment_identifier, as: :attachment
+ expose :author, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :system?, as: :system
+ expose :noteable_id, :noteable_type
+ # upvote? and downvote? are deprecated, always return false
+ expose(:upvote?) { |note| false }
+ expose(:downvote?) { |note| false }
+ end
+
+ class Event < Grape::Entity
+ expose :title, :project_id, :action_name
+ expose :target_id, :target_type, :author_id
+ expose :data, :target_title
+ expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
+
+ expose :author_username do |event, options|
+ event.author&.username
+ end
+ end
+
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
+ end
+
+ class Project < Grape::Entity
+ expose :id, :description, :default_branch, :tag_list
+ expose :public?, as: :public
+ expose :archived?, as: :archived
+ expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group }
+ expose :name, :name_with_namespace
+ expose :path, :path_with_namespace
+ expose :container_registry_enabled
+
+ # Expose old field names with the new permissions methods to keep API compatible
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+ expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+
+ expose :created_at, :last_activity_at
+ expose :shared_runners_enabled
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :creator_id
+ expose :namespace, using: 'API::Entities::Namespace'
+ expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
+ expose :avatar_url
+ expose :star_count, :forks_count
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
+ expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
+ expose :public_builds
+ expose :shared_with_groups do |project, options|
+ ::API::Entities::SharedGroup.represent(project.project_group_links.all, options)
+ end
+ expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds
+ expose :request_access_enabled
+ expose :only_allow_merge_if_all_discussions_are_resolved
+
+ expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics
+ end
+
+ class ProjectWithAccess < Project
+ expose :permissions do
+ expose :project_access, using: ::API::Entities::ProjectAccess do |project, options|
+ project.project_members.find_by(user_id: options[:current_user].id)
+ end
+
+ expose :group_access, using: ::API::Entities::GroupAccess do |project, options|
+ if project.group
+ project.group.group_members.find_by(user_id: options[:current_user].id)
+ end
+ end
+ end
+ end
+
+ class MergeRequest < Grape::Entity
+ expose :id, :iid
+ expose(:project_id) { |entity| entity.project.id }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+ expose :target_branch, :source_branch
+ expose :upvotes, :downvotes
+ expose :author, :assignee, using: ::API::Entities::UserBasic
+ expose :source_project_id, :target_project_id
+ expose :label_names, as: :labels
+ expose :work_in_progress?, as: :work_in_progress
+ expose :milestone, using: ::API::Entities::Milestone
+ expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds
+ expose :merge_status
+ expose :diff_head_sha, as: :sha
+ expose :merge_commit_sha
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+ expose :user_notes_count
+ expose :should_remove_source_branch?, as: :should_remove_source_branch
+ expose :force_remove_source_branch?, as: :force_remove_source_branch
+
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+ end
+
+ class Group < Grape::Entity
+ expose :id, :name, :path, :description, :visibility_level
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :avatar_url
+ expose :web_url
+ expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
+
+ expose :statistics, if: :statistics do
+ with_options format_with: -> (value) { value.to_i } do
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+ end
+ end
+
+ class GroupDetail < Group
+ expose :projects, using: Entities::Project
+ expose :shared_projects, using: Entities::Project
+ end
+
+ class ApplicationSetting < Grape::Entity
+ expose :id
+ expose :default_projects_limit
+ expose :signup_enabled
+ expose :signin_enabled
+ expose :gravatar_enabled
+ expose :sign_in_text
+ expose :after_sign_up_text
+ expose :created_at
+ expose :updated_at
+ expose :home_page_url
+ expose :default_branch_protection
+ expose :restricted_visibility_levels
+ expose :max_attachment_size
+ expose :session_expire_delay
+ expose :default_project_visibility
+ expose :default_snippet_visibility
+ expose :default_group_visibility
+ expose :domain_whitelist
+ expose :domain_blacklist_enabled
+ expose :domain_blacklist
+ expose :user_oauth_applications
+ expose :after_sign_out_path
+ expose :container_registry_token_expire_delay
+ expose :repository_storage
+ expose :repository_storages
+ expose :koding_enabled
+ expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
+ expose :terminal_max_session_time
+ end
+
+ class Environment < ::API::Entities::EnvironmentBasic
+ expose :project, using: Entities::Project
+ end
+
+ class Trigger < Grape::Entity
+ expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ expose :owner, using: ::API::Entities::UserBasic
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: ::API::Entities::User
+ expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :commit, with: ::API::Entities::RepoCommit
+ expose :runner, with: ::API::Entities::Runner
+ expose :pipeline, with: ::API::Entities::PipelineBasic
+ end
+
+ class BuildArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: ::API::Entities::UserBasic
+ expose :environment, using: ::API::Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
+ end
+
+ class MergeRequestChanges < MergeRequest
+ expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+
+ class ProjectService < Grape::Entity
+ expose :id, :title, :created_at, :updated_at, :active
+ expose :push_events, :issues_events, :merge_requests_events
+ expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ # Expose serialized properties
+ expose :properties do |service, options|
+ field_names = service.fields.
+ select { |field| options[:include_passwords] || field[:type] != 'password' }.
+ map { |field| field[:name] }
+ service.properties.slice(*field_names)
+ end
+ end
+
+ class ProjectHook < ::API::Entities::Hook
+ expose :project_id, :issues_events, :merge_requests_events
+ expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ end
end
end
end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
new file mode 100644
index 00000000000..6bb4e016a01
--- /dev/null
+++ b/lib/api/v3/environments.rb
@@ -0,0 +1,87 @@
+module API
+ module V3
+ class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get all environments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ use :pagination
+ end
+ get ':id/environments' do
+ authorize! :read_environment, user_project
+
+ present paginate(user_project.environments), with: Entities::Environment
+ end
+
+ desc 'Creates a new environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the environment to be created'
+ optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
+ end
+ post ':id/environments' do
+ authorize! :create_environment, user_project
+
+ environment = user_project.environments.create(declared_params)
+
+ if environment.persisted?
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
+ desc 'Updates an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ optional :name, type: String, desc: 'The new environment name'
+ optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
+ end
+ put ':id/environments/:environment_id' do
+ authorize! :update_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ update_params = declared_params(include_missing: false).extract!(:name, :external_url)
+ if environment.update(update_params)
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
+ desc 'Deletes an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ delete ':id/environments/:environment_id' do
+ authorize! :update_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ present environment.destroy, with: Entities::Environment
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
index 4f8d58d37c8..13542b0c71c 100644
--- a/lib/api/v3/files.rb
+++ b/lib/api/v3/files.rb
@@ -40,7 +40,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a file from repository'
params do
requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
new file mode 100644
index 00000000000..c5b37622d79
--- /dev/null
+++ b/lib/api/v3/groups.rb
@@ -0,0 +1,181 @@
+module API
+ module V3
+ class Groups < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the group'
+ optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ def present_groups(groups, options = {})
+ options = options.reverse_merge(
+ with: Entities::Group,
+ current_user: current_user,
+ )
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
+ end
+
+ resource :groups do
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ use :statistics_params
+ optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+ optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
+ optional :search, type: String, desc: 'Search for a specific group'
+ optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+ use :pagination
+ end
+ get do
+ groups = if current_user.admin
+ Group.all
+ elsif params[:all_available]
+ GroupsFinder.new.execute(current_user)
+ else
+ current_user.groups
+ end
+
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ groups = groups.reorder(params[:order_by] => params[:sort])
+
+ present_groups groups, statistics: params[:statistics] && current_user.is_admin?
+ end
+
+ desc 'Get list of owned groups for authenticated user' do
+ success Entities::Group
+ end
+ params do
+ use :pagination
+ use :statistics_params
+ end
+ get '/owned' do
+ present_groups current_user.owned_groups, statistics: params[:statistics]
+ end
+
+ desc 'Create a group. Available only for users who can create groups.' do
+ success Entities::Group
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the group'
+ requires :path, type: String, desc: 'The path of the group'
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+ use :optional_params
+ end
+ post do
+ authorize! :create_group
+
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
+
+ if group.persisted?
+ present group, with: Entities::Group, current_user: current_user
+ else
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: { id: %r{[^/]+} } do
+ desc 'Update a group. Available only for users who can administrate groups.' do
+ success Entities::Group
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the group'
+ optional :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ at_least_one_of :name, :path, :description, :visibility_level,
+ :lfs_enabled, :request_access_enabled
+ end
+ put ':id' do
+ group = find_group!(params[:id])
+ authorize! :admin_group, group
+
+ if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
+ present group, with: Entities::GroupDetail, current_user: current_user
+ else
+ render_validation_error!(group)
+ end
+ end
+
+ desc 'Get a single group, with containing projects.' do
+ success Entities::GroupDetail
+ end
+ get ":id" do
+ group = find_group!(params[:id])
+ present group, with: Entities::GroupDetail, current_user: current_user
+ end
+
+ desc 'Remove a group.'
+ delete ":id" do
+ group = find_group!(params[:id])
+ authorize! :admin_group, group
+ present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ end
+
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
+ params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
+ optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+
+ use :pagination
+ end
+ get ":id/projects" do
+ group = find_group!(params[:id])
+ projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = filter_projects(projects)
+ entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
+ present paginate(projects), with: entity, current_user: current_user
+ end
+
+ desc 'Transfer a project to the group namespace. Available only for admin.' do
+ success Entities::GroupDetail
+ end
+ params do
+ requires :project_id, type: String, desc: 'The ID or path of the project'
+ end
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
+ authenticated_as_admin!
+ group = find_group!(params[:id])
+ project = find_project!(params[:project_id])
+ result = ::Projects::TransferService.new(project, current_user).execute(group)
+
+ if result
+ present group, with: Entities::GroupDetail, current_user: current_user
+ else
+ render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb
new file mode 100644
index 00000000000..0f234d4cdad
--- /dev/null
+++ b/lib/api/v3/helpers.rb
@@ -0,0 +1,19 @@
+module API
+ module V3
+ module Helpers
+ def find_project_issue(id)
+ IssuesFinder.new(current_user, project_id: user_project.id).find(id)
+ end
+
+ def find_project_merge_request(id)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ end
+
+ def find_merge_request_with_access(id, access_level = :read_merge_request)
+ merge_request = user_project.merge_requests.find(id)
+ authorize! access_level, merge_request
+ merge_request
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index d0af09f0e1e..54c6a8060b8 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -68,7 +68,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
success ::API::Entities::Issue
end
@@ -89,7 +89,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
@@ -103,7 +103,7 @@ module API
use :issues_params
end
get ":id/issues" do
- project = find_project(params[:id])
+ project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
@@ -139,12 +139,7 @@ module API
end
issue_params = declared_params(include_missing: false)
-
- if merge_request_iid = params[:merge_request_for_resolving_discussions]
- issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
- execute.
- find_by(iid: merge_request_iid)
- end
+ issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
issue = ::Issues::CreateService.new(user_project,
current_user,
@@ -226,6 +221,8 @@ module API
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
+
+ status(200)
issue.destroy
end
end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
index 5c3261311bf..bd5eb2175e8 100644
--- a/lib/api/v3/labels.rb
+++ b/lib/api/v3/labels.rb
@@ -6,13 +6,28 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success ::API::Entities::Label
end
get ':id/labels' do
present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
end
+
+ desc 'Delete an existing label' do
+ success ::API::Entities::Label
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be deleted'
+ end
+ delete ':id/labels' do
+ authorize! :admin_label, user_project
+
+ label = user_project.labels.find_by(title: params[:name])
+ not_found!('Label') unless label
+
+ present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project
+ end
end
end
end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index 4e6cb2e3c52..684860b553e 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success ::API::Entities::Member
end
@@ -86,13 +86,12 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
put ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
+ source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params[:user_id])
- attrs = attributes_for_keys [:access_level, :expires_at]
+ member = source.members.find_by!(user_id: params.delete(:user_id))
- if member.update_attributes(attrs)
+ if member.update_attributes(declared_params(include_missing: false))
present member.user, with: ::API::Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
@@ -120,6 +119,7 @@ module API
# This is to ensure back-compatibility but 204 behavior should be used
# for all DELETE endpoints in 9.0!
if member.nil?
+ status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(source, current_user, declared_params).execute
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 00000000000..35f462e907b
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,44 @@
+module API
+ module V3
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get a list of merge request diff versions' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::MergeRequestDiff
+ end
+
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff
+ end
+
+ desc 'Get a single merge request diff version' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::MergeRequestDiffFull
+ end
+
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 129f9d850e9..3077240e650 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
helpers do
@@ -28,6 +28,14 @@ module API
render_api_error!(errors, 400)
end
+ def issue_entity(project)
+ if project.has_external_issue_tracker?
+ ::API::Entities::ExternalIssue
+ else
+ ::API::Entities::Issue
+ end
+ end
+
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
@@ -39,7 +47,7 @@ module API
desc 'List merge requests' do
detail 'iid filter is deprecated have been removed on V4'
- success ::API::Entities::MergeRequest
+ success ::API::V3::Entities::MergeRequest
end
params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
@@ -66,11 +74,11 @@ module API
end
merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
- present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Create a merge request' do
- success ::API::Entities::MergeRequest
+ success ::API::V3::Entities::MergeRequest
end
params do
requires :title, type: String, desc: 'The title of the merge request'
@@ -89,7 +97,7 @@ module API
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
- present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
@@ -103,6 +111,8 @@ module API
merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
+
+ status(200)
merge_request.destroy
end
@@ -114,12 +124,12 @@ module API
if status == :deprecated
detail DEPRECATION_MESSAGE
end
- success ::API::Entities::MergeRequest
+ success ::API::V3::Entities::MergeRequest
end
get path do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
end
desc 'Get the commits of a merge request' do
@@ -141,7 +151,7 @@ module API
end
desc 'Update a merge request' do
- success ::API::Entities::MergeRequest
+ success ::API::V3::Entities::MergeRequest
end
params do
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
@@ -162,21 +172,21 @@ module API
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
- present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
desc 'Merge a merge request' do
- success ::API::Entities::MergeRequest
+ success ::API::V3::Entities::MergeRequest
end
params do
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
optional :merge_when_build_succeeds, type: Boolean,
- desc: 'When true, this merge request will be merged when the pipeline succeeds'
+ desc: 'When true, this merge request will be merged when the build succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
put "#{path}/merge" do
@@ -209,16 +219,16 @@ module API
.execute(merge_request)
end
- present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project
end
- desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
- success ::API::Entities::MergeRequest
+ desc 'Cancel merge if "Merge When Build succeeds" is enabled' do
+ success ::API::V3::Entities::MergeRequest
end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = find_project_merge_request(params[:merge_request_id])
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
::MergeRequest::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
new file mode 100644
index 00000000000..be90cec4afc
--- /dev/null
+++ b/lib/api/v3/milestones.rb
@@ -0,0 +1,64 @@
+module API
+ module V3
+ class Milestones < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def filter_milestones_state(milestones, state)
+ case state
+ when 'active' then milestones.active
+ when 'closed' then milestones.closed
+ else milestones
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get a list of project milestones' do
+ success ::API::Entities::Milestone
+ end
+ params do
+ optional :state, type: String, values: %w[active closed all], default: 'all',
+ desc: 'Return "active", "closed", or "all" milestones'
+ optional :iid, type: Array[Integer], desc: 'The IID of the milestone'
+ use :pagination
+ end
+ get ":id/milestones" do
+ authorize! :read_milestone, user_project
+
+ milestones = user_project.milestones
+ milestones = filter_milestones_state(milestones, params[:state])
+ milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+
+ present paginate(milestones), with: ::API::Entities::Milestone
+ end
+
+ desc 'Get all issues for a single project milestone' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/issues' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_title: milestone.title
+ }
+
+ issues = IssuesFinder.new(current_user, finder_params).execute
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
new file mode 100644
index 00000000000..4f8e0eff4ff
--- /dev/null
+++ b/lib/api/v3/notes.rb
@@ -0,0 +1,148 @@
+module API
+ module V3
+ class Notes < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ NOTEABLE_TYPES.each do |noteable_type|
+ noteables_str = noteable_type.to_s.underscore.pluralize
+
+ desc 'Get a list of project +noteable+ notes' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ use :pagination
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(noteable.notes).
+ reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: ::API::V3::Entities::Note
+ else
+ not_found!("Notes")
+ end
+ end
+
+ desc 'Get a single +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ note = noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+
+ if can_read_note
+ present note, with: ::API::V3::Entities::Note
+ else
+ not_found!("Note")
+ end
+ end
+
+ desc 'Create a new +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/notes" do
+ opts = {
+ note: params[:body],
+ noteable_type: noteables_str.classify,
+ noteable_id: params[:noteable_id]
+ }
+
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+ opts[:created_at] = params[:created_at]
+ end
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ if note.valid?
+ present note, with: ::API::V3::Entities.const_get(note.class.name)
+ else
+ not_found!("Note #{note.errors.messages}")
+ end
+ else
+ not_found!("Note")
+ end
+ end
+
+ desc 'Update an existing +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ note = user_project.notes.find(params[:note_id])
+
+ authorize! :admin_note, note
+
+ opts = {
+ note: params[:body]
+ }
+
+ note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+
+ if note.valid?
+ present note, with: ::API::V3::Entities::Note
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'Delete a +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ note = user_project.notes.find(params[:note_id])
+ authorize! :admin_note, note
+
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
+
+ present note, with: ::API::V3::Entities::Note
+ end
+ end
+ end
+
+ helpers do
+ def noteable_read_ability_name(noteable)
+ "read_#{noteable.class.to_s.underscore}".to_sym
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
new file mode 100644
index 00000000000..82827249244
--- /dev/null
+++ b/lib/api/v3/pipelines.rb
@@ -0,0 +1,36 @@
+module API
+ module V3
+ class Pipelines < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get all Pipelines of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success ::API::Entities::Pipeline
+ end
+ params do
+ use :pagination
+ optional :scope, type: String, values: %w(running branches tags),
+ desc: 'Either running, branches, or tags'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+
+ pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+ present paginate(pipelines), with: ::API::Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
new file mode 100644
index 00000000000..94614bfc8b6
--- /dev/null
+++ b/lib/api/v3/project_hooks.rb
@@ -0,0 +1,106 @@
+module API
+ module V3
+ class ProjectHooks < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ params :project_hook_properties do
+ requires :url, type: String, desc: "The URL to send the request to"
+ optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+ optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
+ optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+ optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
+ optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+ optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
+ optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
+ optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+ optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get project hooks' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ use :pagination
+ end
+ get ":id/hooks" do
+ hooks = paginate user_project.hooks
+
+ present hooks, with: ::API::V3::Entities::ProjectHook
+ end
+
+ desc 'Get a project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: 'The ID of a project hook'
+ end
+ get ":id/hooks/:hook_id" do
+ hook = user_project.hooks.find(params[:hook_id])
+ present hook, with: ::API::V3::Entities::ProjectHook
+ end
+
+ desc 'Add hook to project' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ use :project_hook_properties
+ end
+ post ":id/hooks" do
+ hook = user_project.hooks.new(declared_params(include_missing: false))
+
+ if hook.save
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Update an existing project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+ use :project_hook_properties
+ end
+ put ":id/hooks/:hook_id" do
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ if hook.update_attributes(declared_params(include_missing: false))
+ present hook, with: ::API::V3::Entities::ProjectHook
+ else
+ error!("Invalid url given", 422) if hook.errors[:url].present?
+
+ not_found!("Project hook #{hook.errors.messages}")
+ end
+ end
+
+ desc 'Deletes project hook' do
+ success ::API::V3::Entities::ProjectHook
+ end
+ params do
+ requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
+ end
+ delete ":id/hooks/:hook_id" do
+ begin
+ present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook
+ rescue
+ # ProjectHook can raise Error if hook_id not found
+ not_found!("Error deleting hook #{params[:hook_id]}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index e03e941d30b..fc065a22d74 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -121,6 +121,8 @@ module API
authorize! :admin_project_snippet, snippet
snippet.destroy
+
+ status(200)
end
desc 'Get a raw project snippet'
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 6796da83f07..b753dbab381 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -5,6 +5,10 @@ module API
before { authenticate_non_get! }
+ after_validation do
+ set_only_allow_merge_if_pipeline_succeeds!
+ end
+
helpers do
params :optional_params do
optional :description, type: String, desc: 'The description of the project'
@@ -20,10 +24,12 @@ module API
optional :visibility_level, type: Integer, values: [
Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+ Gitlab::VisibilityLevel::PUBLIC
+ ], desc: 'Create a public project. The same as visibility_level = 20.'
optional :public_builds, type: Boolean, desc: 'Perform public builds'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
end
@@ -36,6 +42,12 @@ module API
end
attrs
end
+
+ def set_only_allow_merge_if_pipeline_succeeds!
+ if params.has_key?(:only_allow_merge_if_build_succeeds)
+ params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
+ end
+ end
end
resource :projects do
@@ -74,7 +86,7 @@ module API
def present_projects(projects, options = {})
options = options.reverse_merge(
- with: ::API::Entities::Project,
+ with: ::API::V3::Entities::Project,
current_user: current_user,
simple: params[:simple],
)
@@ -94,7 +106,7 @@ module API
use :collection_params
end
get '/visible' do
- entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity
end
@@ -108,7 +120,7 @@ module API
authenticate!
present_projects current_user.authorized_projects,
- with: ::API::Entities::ProjectWithAccess
+ with: ::API::V3::Entities::ProjectWithAccess
end
desc 'Get an owned projects list for authenticated user' do
@@ -122,7 +134,7 @@ module API
authenticate!
present_projects current_user.owned_projects,
- with: ::API::Entities::ProjectWithAccess,
+ with: ::API::V3::Entities::ProjectWithAccess,
statistics: params[:statistics]
end
@@ -148,11 +160,11 @@ module API
get '/all' do
authenticated_as_admin!
- present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics]
+ present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics]
end
desc 'Search for projects the current user has access to' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
params do
requires :query, type: String, desc: 'The project name to be searched'
@@ -164,15 +176,16 @@ module API
projects = search_service.objects('projects', params[:page])
projects = projects.reorder(params[:order_by] => params[:sort])
- present paginate(projects), with: ::API::Entities::Project
+ present paginate(projects), with: ::API::V3::Entities::Project
end
desc 'Create new project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
params do
- requires :name, type: String, desc: 'The name of the project'
+ optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
use :optional_params
use :create_params
end
@@ -181,7 +194,7 @@ module API
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
- present project, with: ::API::Entities::Project,
+ present project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project)
else
if project.errors[:limit_reached].present?
@@ -192,7 +205,7 @@ module API
end
desc 'Create new project for a specified user. Only available to admin users.' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
params do
requires :name, type: String, desc: 'The name of the project'
@@ -210,7 +223,7 @@ module API
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
- present project, with: ::API::Entities::Project,
+ present project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project)
else
render_validation_error!(project)
@@ -221,28 +234,28 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: /[^\/]+/ } do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
- success ::API::Entities::ProjectWithAccess
+ success ::API::V3::Entities::ProjectWithAccess
end
get ":id" do
- entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project)
end
desc 'Get events for a single project' do
- success ::API::Entities::Event
+ success ::API::V3::Entities::Event
end
params do
use :pagination
end
get ":id/events" do
- present paginate(user_project.events.recent), with: ::API::Entities::Event
+ present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
end
desc 'Fork new project for the current user or provided namespace.' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
params do
optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
@@ -268,13 +281,13 @@ module API
if forked_project.errors.any?
conflict!(forked_project.errors.messages)
else
- present forked_project, with: ::API::Entities::Project,
+ present forked_project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, forked_project)
end
end
desc 'Update an existing project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
params do
optional :name, type: String, desc: 'The name of the project'
@@ -298,7 +311,7 @@ module API
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if result[:status] == :success
- present user_project, with: ::API::Entities::Project,
+ present user_project, with: ::API::V3::Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project)
else
render_validation_error!(user_project)
@@ -306,29 +319,29 @@ module API
end
desc 'Archive a project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
post ':id/archive' do
authorize!(:archive_project, user_project)
user_project.archive!
- present user_project, with: ::API::Entities::Project
+ present user_project, with: ::API::V3::Entities::Project
end
desc 'Unarchive a project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
post ':id/unarchive' do
authorize!(:archive_project, user_project)
user_project.unarchive!
- present user_project, with: ::API::Entities::Project
+ present user_project, with: ::API::V3::Entities::Project
end
desc 'Star a project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
post ':id/star' do
if current_user.starred?(user_project)
@@ -337,19 +350,19 @@ module API
current_user.toggle_star(user_project)
user_project.reload
- present user_project, with: ::API::Entities::Project
+ present user_project, with: ::API::V3::Entities::Project
end
end
desc 'Unstar a project' do
- success ::API::Entities::Project
+ success ::API::V3::Entities::Project
end
delete ':id/star' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
- present user_project, with: ::API::Entities::Project
+ present user_project, with: ::API::V3::Entities::Project
else
not_modified!
end
@@ -358,6 +371,8 @@ module API
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
+
+ status(200)
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
@@ -383,6 +398,7 @@ module API
authorize! :remove_fork_project, user_project
if user_project.forked?
+ status(200)
user_project.forked_project_link.destroy
else
not_modified!
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 3549ea225ef..e4d14bc8168 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -38,6 +38,60 @@ module API
present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
end
+ desc 'Get a raw file contents'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :filepath, type: String, desc: 'The path to the file to display'
+ end
+ get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
+ repo = user_project.repository
+ commit = repo.commit(params[:sha])
+ not_found! "Commit" unless commit
+ blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
+ not_found! "File" unless blob
+ send_git_blob repo, blob
+ end
+
+ desc 'Get a raw blob contents by blob sha'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ end
+ get ':id/repository/raw_blobs/:sha' do
+ repo = user_project.repository
+ begin
+ blob = Gitlab::Git::Blob.raw(repo, params[:sha])
+ rescue
+ not_found! 'Blob'
+ end
+ not_found! 'Blob' unless blob
+ send_git_blob repo, blob
+ end
+
+ desc 'Get an archive of the repository'
+ params do
+ optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
+ optional :format, type: String, desc: 'The archive format'
+ end
+ get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ begin
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
+ rescue
+ not_found!('File')
+ end
+ end
+
+ desc 'Compare two branches, tags, or commits' do
+ success ::API::Entities::Compare
+ end
+ params do
+ requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
+ requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+ end
+ get ':id/repository/compare' do
+ compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
+ present compare, with: ::API::Entities::Compare
+ end
+
desc 'Get repository contributors' do
success ::API::Entities::Contributor
end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
new file mode 100644
index 00000000000..1934d6e578c
--- /dev/null
+++ b/lib/api/v3/runners.rb
@@ -0,0 +1,65 @@
+module API
+ module V3
+ class Runners < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :runners do
+ desc 'Remove a runner' do
+ success ::API::Entities::Runner
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
+ delete ':id' do
+ runner = Ci::Runner.find(params[:id])
+ not_found!('Runner') unless runner
+
+ authenticate_delete_runner!(runner)
+
+ status(200)
+ runner.destroy
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ before { authorize_admin_project }
+
+ desc "Disable project's runner" do
+ success ::API::Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
+ delete ':id/runners/:runner_id' do
+ runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
+ not_found!('Runner') unless runner_project
+
+ runner = runner_project.runner
+ forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
+
+ runner_project.destroy
+
+ present runner, with: ::API::Entities::Runner
+ end
+ end
+
+ helpers do
+ def authenticate_delete_runner!(runner)
+ return if current_user.is_admin?
+ forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner associated with more than one project") if runner.projects.count > 1
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def user_can_access_runner?(runner)
+ current_user.ci_authorized_runners.exists?(runner.id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
new file mode 100644
index 00000000000..3bacaeee032
--- /dev/null
+++ b/lib/api/v3/services.rb
@@ -0,0 +1,644 @@
+module API
+ module V3
+ class Services < Grape::API
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ }
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'mattermost' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }
+
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ]
+ }.freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
+ end
+ end
+ end
+
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ status(200)
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
+ end
+ end
+
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ end
+ end
+
+ trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
+
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
+
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
+
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
new file mode 100644
index 00000000000..748d6b97d4f
--- /dev/null
+++ b/lib/api/v3/settings.rb
@@ -0,0 +1,137 @@
+module API
+ module V3
+ class Settings < Grape::API
+ before { authenticated_as_admin! }
+
+ helpers do
+ def current_settings
+ @current_setting ||=
+ (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ end
+ end
+
+ desc 'Get the current application settings' do
+ success Entities::ApplicationSetting
+ end
+ get "application/settings" do
+ present current_settings, with: Entities::ApplicationSetting
+ end
+
+ desc 'Modify application settings' do
+ success Entities::ApplicationSetting
+ end
+ params do
+ optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
+ optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
+ optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+ optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
+ optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
+ optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
+ optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
+ optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
+ optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
+ optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
+ optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
+ given domain_blacklist_enabled: ->(val) { val } do
+ requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ end
+ optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled'
+ optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
+ given require_two_factor_authentication: ->(val) { val } do
+ requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
+ end
+ optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
+ optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
+ optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
+ optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
+ given shared_runners_enabled: ->(val) { val } do
+ requires :shared_runners_text, type: String, desc: 'Shared runners text '
+ end
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+ optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
+ optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
+ given metrics_enabled: ->(val) { val } do
+ requires :metrics_host, type: String, desc: 'The InfluxDB host'
+ requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
+ requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
+ requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
+ requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
+ requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
+ requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
+ end
+ optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
+ given sidekiq_throttling_enabled: ->(val) { val } do
+ requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
+ requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
+ end
+ optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
+ given recaptcha_enabled: ->(val) { val } do
+ requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
+ requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
+ end
+ optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
+ given akismet_enabled: ->(val) { val } do
+ requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
+ end
+ optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
+ optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
+ given sentry_enabled: ->(val) { val } do
+ requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
+ end
+ optional :repository_storage, type: String, desc: 'Storage paths for new projects'
+ optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
+ optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
+ given koding_enabled: ->(val) { val } do
+ requires :koding_url, type: String, desc: 'The Koding team URL'
+ end
+ optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+ given plantuml_enabled: ->(val) { val } do
+ requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+ end
+ optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
+ optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
+ optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
+ optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
+ given housekeeping_enabled: ->(val) { val } do
+ requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
+ requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
+ requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
+ requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
+ end
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
+ :default_group_visibility, :restricted_visibility_levels, :import_sources,
+ :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
+ :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
+ :user_oauth_applications, :user_default_external, :signup_enabled,
+ :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
+ :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
+ :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
+ :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+ :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
+ :akismet_enabled, :admin_notification_email, :sentry_enabled,
+ :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
+ :version_check_enabled, :email_author_in_body, :html_emails_enabled,
+ :housekeeping_enabled, :terminal_max_session_time
+ end
+ put "application/settings" do
+ if current_settings.update_attributes(declared_params(include_missing: false))
+ present current_settings, with: Entities::ApplicationSetting
+ else
+ render_validation_error!(current_settings)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
new file mode 100644
index 00000000000..07dac7e9904
--- /dev/null
+++ b/lib/api/v3/snippets.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: ::API::Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: ::API::Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success ::API::Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
index 02a4157c26e..068750ec077 100644
--- a/lib/api/v3/subscriptions.rb
+++ b/lib/api/v3/subscriptions.rb
@@ -14,7 +14,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = ::API::Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
index 391510b9ee0..5787c06fc12 100644
--- a/lib/api/v3/system_hooks.rb
+++ b/lib/api/v3/system_hooks.rb
@@ -13,6 +13,19 @@ module API
get do
present SystemHook.all, with: ::API::Entities::Hook
end
+
+ desc 'Delete a hook' do
+ success ::API::Entities::Hook
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the system hook'
+ end
+ delete ":id" do
+ hook = SystemHook.find_by(id: params[:id])
+ not_found!('System hook') unless hook
+
+ present hook.destroy, with: ::API::Entities::Hook
+ end
end
end
end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 016e3d86932..c2541de2f50 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success ::API::Entities::RepoTag
end
@@ -14,6 +14,26 @@ module API
tags = user_project.repository.tags.sort_by(&:name).reverse
present tags, with: ::API::Entities::RepoTag, project: user_project
end
+
+ desc 'Delete a repository tag'
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
+ delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
+ authorize_push_project
+
+ result = ::Tags::DestroyService.new(user_project, current_user).
+ execute(params[:tag_name])
+
+ if result[:status] == :success
+ status(200)
+ {
+ tag_name: params[:tag_name]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
end
end
end
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..81ae4e8137d
--- /dev/null
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -0,0 +1,116 @@
+module API
+ module V3
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: ::API::Entities::IssuableTimeStats
+ else
+ render_validation_error!(issuable)
+ end
+ end
+
+ def update_service
+ issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+ end
+ end
+
+ issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
+ issuable_collection_name = issuable_name.pluralize
+ issuable_key = "#{issuable_name}_id".to_sym
+
+ desc "Set a time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+ end
+
+ desc "Reset the time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: 0)
+ end
+
+ desc "Add spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ update_issuable(spend_time: {
+ duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+ user: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: ::API::Entities::IssuableTimeStats
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
index 4f9b5fe72a6..e3b311d61cd 100644
--- a/lib/api/v3/todos.rb
+++ b/lib/api/v3/todos.rb
@@ -19,8 +19,10 @@ module API
desc 'Mark all todos as done'
delete do
+ status(200)
+
todos = TodosFinder.new(current_user, params).execute
- TodoService.new.mark_todos_as_done(todos, current_user)
+ TodoService.new.mark_todos_as_done(todos, current_user).size
end
end
end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
new file mode 100644
index 00000000000..a23d6b6b48c
--- /dev/null
+++ b/lib/api/v3/triggers.rb
@@ -0,0 +1,103 @@
+module API
+ module V3
+ class Triggers < Grape::API
+ include PaginationParams
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Trigger a GitLab project build' do
+ success ::API::V3::Entities::TriggerRequest
+ end
+ params do
+ requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+ requires :token, type: String, desc: 'The unique token of trigger'
+ optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+ end
+ post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
+ project = find_project(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: ::API::V3::Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+
+ desc 'Get triggers list' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ use :pagination
+ end
+ get ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+
+ present paginate(triggers), with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Get specific trigger of a project' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
+ get ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Create a trigger' do
+ success ::API::V3::Entities::Trigger
+ end
+ post ':id/triggers' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.create
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+
+ desc 'Delete a trigger' do
+ success ::API::V3::Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
+ delete ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ trigger.destroy
+
+ present trigger, with: ::API::V3::Entities::Trigger
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
index e05e457a5df..14f54731730 100644
--- a/lib/api/v3/users.rb
+++ b/lib/api/v3/users.rb
@@ -71,6 +71,46 @@ module API
user.activate
end
end
+
+ desc 'Get the contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success ::API::V3::Entities::Event
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/events' do
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ events = user.events.
+ merge(ProjectsFinder.new.execute(current_user)).
+ references(:project).
+ with_associations.
+ recent
+
+ present paginate(events), with: ::API::V3::Entities::Event
+ end
+
+ desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete ':id/keys/:key_id' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: ::API::Entities::SSHKey
+ end
end
resource :user do
@@ -90,6 +130,19 @@ module API
get "emails" do
present current_user.emails, with: ::API::Entities::Email
end
+
+ desc 'Delete an SSH key from the currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: ::API::Entities::SSHKey
+ end
end
end
end
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
new file mode 100644
index 00000000000..83972b1e7ce
--- /dev/null
+++ b/lib/api/v3/variables.rb
@@ -0,0 +1,29 @@
+module API
+ module V3
+ class Variables < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize! :admin_build, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Delete an existing variable from a project' do
+ success ::API::Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key])
+ not_found!('Variable') unless variable
+
+ present variable.destroy, with: ::API::Entities::Variable
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f623b1dfe9f..5acde41551b 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,5 +1,4 @@
module API
- # Projects variables API
class Variables < Grape::API
include PaginationParams
@@ -10,7 +9,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project variables' do
success Entities::Variable
end
@@ -81,10 +80,9 @@ module API
end
delete ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
+ not_found!('Variable') unless variable
- return not_found!('Variable') unless variable
-
- present variable.destroy, with: Entities::Variable
+ variable.destroy
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 22319ec6623..4016ac76348 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -5,7 +5,7 @@ module Backup
attr_reader :config, :db_file_name
def initialize
- @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
+ @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
@@ -13,28 +13,32 @@ module Backup
FileUtils.mkdir_p(File.dirname(db_file_name))
FileUtils.rm_f(db_file_name)
compress_rd, compress_wr = IO.pipe
- compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+ compress_pid = spawn(*%w(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
compress_rd.close
- dump_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Dumping MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
- when "postgresql" then
- $progress.print "Dumping PostgreSQL database #{config['database']} ... "
- pg_env
- pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
- if Gitlab.config.backup.pg_schema
- pgsql_args << "-n"
- pgsql_args << Gitlab.config.backup.pg_schema
+ dump_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Dumping MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
+ when "postgresql" then
+ $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ pg_env
+ pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+ if Gitlab.config.backup.pg_schema
+ pgsql_args << "-n"
+ pgsql_args << Gitlab.config.backup.pg_schema
+ end
+ spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
end
- spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
- end
compress_wr.close
- success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [compress_pid, dump_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Backup failed' unless success
@@ -42,23 +46,27 @@ module Backup
def restore
decompress_rd, decompress_wr = IO.pipe
- decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
decompress_wr.close
- restore_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
- when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
- pg_env
- spawn('psql', config['database'], in: decompress_rd)
- end
+ restore_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Restoring MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
+ when "postgresql" then
+ $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ pg_env
+ spawn('psql', config['database'], in: decompress_rd)
+ end
decompress_rd.close
- success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [decompress_pid, restore_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Restore failed' unless success
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 247c32c1c0a..30a91647b77 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -26,10 +26,10 @@ module Backup
abort 'Backup failed'
end
- run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
- run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
end
end
@@ -37,7 +37,7 @@ module Backup
backup_existing_files_dir
create_files_dir
- run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
end
def backup_existing_files_dir
@@ -47,7 +47,7 @@ module Backup
end
end
- def run_pipeline!(cmd_list, options={})
+ def run_pipeline!(cmd_list, options = {})
status_list = Open3.pipeline(*cmd_list, options)
abort 'Backup failed' unless status_list.compact.all?(&:success?)
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index f099c0651ac..7b4476fa4db 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,8 +1,8 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
- FOLDERS_TO_BACKUP = %w[repositories db]
- FILE_NAME_SUFFIX = '_gitlab_backup.tar'
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
+ FOLDERS_TO_BACKUP = %w[repositories db].freeze
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
def pack
# Make sure there is a connection
@@ -20,13 +20,13 @@ module Backup
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
"w+") do |file|
- file << s.to_yaml.gsub(/^---\n/,'')
+ file << s.to_yaml.gsub(/^---\n/, '')
end
# create archive
$progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races.
- tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
+ tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
$progress.puts "done".color(:green)
else
@@ -50,8 +50,9 @@ module Backup
directory = connect_to_remote_directory(connection_settings)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
- multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
- encryption: Gitlab.config.backup.upload.encryption)
+ multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+ encryption: Gitlab.config.backup.upload.encryption,
+ storage_class: Gitlab.config.backup.upload.storage_class)
$progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
@@ -123,11 +124,11 @@ module Backup
exit 1
end
- if ENV['BACKUP'].present?
- tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
- else
- tar_file = file_list.first
- end
+ tar_file = if ENV['BACKUP'].present?
+ "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ file_list.first
+ end
unless File.exist?(tar_file)
$progress.puts "The backup file #{tar_file} does not exist!"
@@ -158,7 +159,7 @@ module Backup
end
def tar_version
- tar_version, _ = Gitlab::Popen.popen(%W(tar --version))
+ tar_version, _ = Gitlab::Popen.popen(%w(tar --version))
tar_version.force_encoding('locale').split("\n").first
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 91e43dcb114..cd745d35e7c 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,7 +2,7 @@ require 'yaml'
module Backup
class Repository
-
+ # rubocop:disable Metrics/AbcSize
def dump
prepare
@@ -68,7 +68,8 @@ module Backup
end
def restore
- Gitlab.config.repositories.storages.each do |name, path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ path = repository_storage['path']
next unless File.exist?(path)
# Move repos dir to 'repositories.old' dir
@@ -85,11 +86,11 @@ module Backup
project.ensure_dir_exist
- if File.exists?(path_to_project_bundle)
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
- else
- cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
- end
+ cmd = if File.exist?(path_to_project_bundle)
+ %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
+ else
+ %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
+ end
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
@@ -150,6 +151,7 @@ module Backup
puts output
end
end
+ # rubocop:enable Metrics/AbcSize
protected
@@ -179,9 +181,8 @@ module Backup
return unless Dir.exist?(path)
dir_entries = Dir.entries(path)
- %w[annex custom_hooks].each do |entry|
- yield(entry) if dir_entries.include?(entry)
- end
+
+ yield('custom_hooks') if dir_entries.include?('custom_hooks')
end
def prepare
@@ -193,13 +194,13 @@ module Backup
end
def silent
- {err: '/dev/null', out: '/dev/null'}
+ { err: '/dev/null', out: '/dev/null' }
end
private
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
end
end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 9261f77f3c9..35118375499 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,6 @@ require 'backup/files'
module Backup
class Uploads < Files
-
def initialize
super('uploads', Rails.root.join('public/uploads'))
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 3b15ff6566f..8bc2dd18bda 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -160,11 +160,12 @@ module Banzai
data = data_attributes_for(link_content || match, project, object, link: !!link_content)
- if matches.names.include?("url") && matches[:url]
- url = matches[:url]
- else
- url = url_for_object_cached(object, project)
- end
+ url =
+ if matches.names.include?("url") && matches[:url]
+ matches[:url]
+ else
+ url_for_object_cached(object, project)
+ end
content = link_content || object_link_text(object, matches)
@@ -238,18 +239,13 @@ module Banzai
# path.
def projects_per_reference
@projects_per_reference ||= begin
- hash = {}
refs = Set.new
references_per_project.each do |project_ref, _|
refs << project_ref
end
- find_projects_for_paths(refs.to_a).each do |project|
- hash[project.path_with_namespace] = project
- end
-
- hash
+ find_projects_for_paths(refs.to_a).index_by(&:full_path)
end
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 80c844baecd..b8d2673c1a6 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -37,7 +37,7 @@ module Banzai
and contains(., '://')
and not(starts-with(., 'http'))
and not(starts-with(., 'ftp'))
- ])
+ ]).freeze
def call
return doc if context[:autolink] == false
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index a8c1ca0c60a..d6138816e70 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -17,8 +17,8 @@ module Banzai
next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
- html = emoji_name_image_filter(content)
- html = emoji_unicode_image_filter(html)
+ html = emoji_unicode_element_unicode_filter(content)
+ html = emoji_name_element_unicode_filter(html)
next if html == content
@@ -27,33 +27,30 @@ module Banzai
doc
end
- # Replace :emoji: with corresponding images.
+ # Replace :emoji: with corresponding gl-emoji unicode.
#
# text - String text to replace :emoji: in.
#
- # Returns a String with :emoji: replaced with images.
- def emoji_name_image_filter(text)
+ # Returns a String with :emoji: replaced with gl-emoji unicode.
+ def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
- emoji_image_tag(name, emoji_url(name))
+ Gitlab::Emoji.gl_emoji_tag(name)
end
end
- # Replace unicode emoji with corresponding images if they exist.
+ # Replace unicode emoji with corresponding gl-emoji unicode.
#
# text - String text to replace unicode emoji in.
#
- # Returns a String with unicode emoji replaced with images.
- def emoji_unicode_image_filter(text)
+ # Returns a String with unicode emoji replaced with gl-emoji unicode.
+ def emoji_unicode_element_unicode_filter(text)
text.gsub(emoji_unicode_pattern) do |moji|
- emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
+ emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
+ Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
end
end
- def emoji_image_tag(emoji_name, emoji_url)
- "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
- end
-
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
@@ -66,52 +63,13 @@ module Banzai
private
- def emoji_url(name)
- emoji_path = emoji_filename(name)
-
- if context[:asset_host]
- # Asset host is specified.
- url_to_image(emoji_path)
- elsif context[:asset_root]
- # Gitlab url is specified
- File.join(context[:asset_root], url_to_image(emoji_path))
- else
- # All other cases
- url_to_image(emoji_path)
- end
- end
-
- def emoji_unicode_url(moji)
- emoji_unicode_path = emoji_unicode_filename(moji)
-
- if context[:asset_host]
- url_to_image(emoji_unicode_path)
- elsif context[:asset_root]
- File.join(context[:asset_root], url_to_image(emoji_unicode_path))
- else
- url_to_image(emoji_unicode_path)
- end
- end
-
- def url_to_image(image)
- ActionController::Base.helpers.url_to_image(image)
- end
-
def emoji_pattern
self.class.emoji_pattern
end
- def emoji_filename(name)
- "#{Gitlab::Emoji.emoji_filename(name)}.png"
- end
-
def emoji_unicode_pattern
self.class.emoji_unicode_pattern
end
-
- def emoji_unicode_filename(name)
- "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
- end
end
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index d08267a9d6c..0ea4eeaed5b 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -149,11 +149,12 @@ module Banzai
name, reference = *parts.compact.map(&:strip)
end
- if url?(reference)
- href = reference
- else
- href = ::File.join(project_wiki_base_path, reference)
- end
+ href =
+ if url?(reference)
+ reference
+ else
+ ::File.join(project_wiki_base_path, reference)
+ end
content_tag(:a, name || reference, href: href, class: 'gfm')
end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index f0fb6084a35..123c92fd250 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -2,29 +2,22 @@ module Banzai
module Filter
# HTML filter that wraps links around inline images.
class ImageLinkFilter < HTML::Pipeline::Filter
-
# Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image as a child
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
- div = doc.document.create_element(
- 'div',
- class: 'image-container'
- )
-
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
href: img['src'],
- target: '_blank'
+ target: '_blank',
+ rel: 'noopener noreferrer'
)
link.children = img.clone
- div.children = link
-
- img.replace(div)
+ img.replace(link)
end
doc
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index fd6b9704132..044d18ff824 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -39,11 +39,12 @@ module Banzai
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
- if project.default_issues_tracker?
- issues = project.issues.where(iid: issue_ids.to_a)
- else
- issues = issue_ids.map { |id| ExternalIssue.new(id, project) }
- end
+ issues =
+ if project.default_issues_tracker?
+ project.issues.where(iid: issue_ids.to_a)
+ else
+ issue_ids.map { |id| ExternalIssue.new(id, project) }
+ end
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index af1e575fc89..d5f9e252f62 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -35,6 +35,10 @@ module Banzai
# Allow span elements
whitelist[:elements].push('span')
+ # Allow html5 details/summary elements
+ whitelist[:elements].push('details')
+ whitelist[:elements].push('summary')
+
# Allow abbr elements with title attribute
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index a447e2b8bff..9f09ca90697 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -5,8 +5,6 @@ module Banzai
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
- include Rouge::Plugins::Redcarpet
-
def call
doc.search('pre > code').each do |node|
highlight_node(node)
@@ -23,7 +21,7 @@ module Banzai
lang = lexer.tag
begin
- code = format(lex(lexer, code))
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
css_classes << " js-syntax-highlight #{lang}"
rescue
@@ -45,10 +43,6 @@ module Banzai
lexer.lex(code)
end
- def format(tokens)
- rouge_formatter.format(tokens)
- end
-
def lexer_for(language)
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
@@ -57,11 +51,6 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
-
- # Override Rouge::Plugins::Redcarpet#rouge_formatter
- def rouge_formatter(lexer = nil)
- @rouge_formatter ||= Rouge::Formatters::HTML.new
- end
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index c973897f420..fe1f0923136 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -74,10 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||=
- Namespace.where_full_path_in(usernames).each_with_object({}) do |row, hash|
- hash[row.full_path] = row
- end
+ @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path)
end
# Returns all usernames referenced in the current document.
@@ -133,7 +130,7 @@ module Banzai
data = data_attribute(group: namespace.id)
content = link_content || Group.reference_prefix + group
- link_tag(url, data, content, namespace.name)
+ link_tag(url, data, content, namespace.full_name)
end
def link_to_user(user, namespace, link_content: nil)
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index b64a1287d4d..35cb10eae5d 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -43,6 +43,7 @@ module Banzai
element['title'] || element['alt'],
href: element['src'],
target: '_blank',
+ rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'")
download_paragraph = doc.document.create_element('p')
download_paragraph.children = link
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index b25d6f18d59..fd4a6a107c2 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -2,10 +2,10 @@ module Banzai
module Pipeline
class GfmPipeline < BasePipeline
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js
# consequently convert that same HTML to GFM to be copied to the clipboard.
# Every filter that generates HTML from GFM should have a handler in
- # app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
+ # app/assets/javascripts/copy_as_gfm.js, in reverse order.
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index 2058a58d0ae..52fdb9a2140 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -134,9 +134,7 @@ module Banzai
ids = unique_attribute_values(nodes, attribute)
rows = collection_objects_for_ids(collection, ids)
- rows.each_with_object({}) do |row, hash|
- hash[row.id] = row
- end
+ rows.index_by(&:id)
end
# Returns an Array containing all unique values of an attribute of the
@@ -210,7 +208,7 @@ module Banzai
grouped_objects_for_nodes(nodes, Project, 'data-project')
end
- def can?(user, permission, subject)
+ def can?(user, permission, subject = :global)
Ability.allowed?(user, permission, subject)
end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 7e55cf4deab..b9279c33f5b 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -1,8 +1,8 @@
module Bitbucket
class Connection
- DEFAULT_API_VERSION = '2.0'
- DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
- DEFAULT_QUERY = {}
+ DEFAULT_API_VERSION = '2.0'.freeze
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'.freeze
+ DEFAULT_QUERY = {}.freeze
attr_reader :expires_at, :expires_in, :refresh_token, :token
@@ -24,9 +24,7 @@ module Bitbucket
response.parsed
end
- def expired?
- connection.expired?
- end
+ delegate :expired?, to: :connection
def refresh!
response = connection.refresh!
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
index 5e2eb57bb0e..efe10542f19 100644
--- a/lib/bitbucket/error/unauthorized.rb
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -1,6 +1,5 @@
module Bitbucket
module Error
- class Unauthorized < StandardError
- end
+ Unauthorized = Class.new(StandardError)
end
end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 423eff8f2a5..59b0fda8e14 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -23,7 +23,7 @@ module Bitbucket
url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
if token.present?
- clone_url = URI::parse(url)
+ clone_url = URI.parse(url)
clone_url.user = "x-token-auth:#{token}"
clone_url.to_s
else
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 158a33f26fe..b3ccad7b28d 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -13,7 +13,7 @@ module Ci
5 => 'magenta',
6 => 'cyan',
7 => 'white', # not that this is gray in the dark (aka default) color table
- }
+ }.freeze
STYLE_SWITCHES = {
bold: 0x01,
@@ -21,7 +21,7 @@ module Ci
underline: 0x04,
conceal: 0x08,
cross: 0x10,
- }
+ }.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
@@ -29,64 +29,108 @@ module Ci
class Converter
def on_0(s) reset() end
+
def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+
def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+
def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+
def on_9(s) enable(STYLE_SWITCHES[:cross]) end
def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+
def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+
def on_29(s) disable(STYLE_SWITCHES[:cross]) end
def on_30(s) set_fg_color(0) end
+
def on_31(s) set_fg_color(1) end
+
def on_32(s) set_fg_color(2) end
+
def on_33(s) set_fg_color(3) end
+
def on_34(s) set_fg_color(4) end
+
def on_35(s) set_fg_color(5) end
+
def on_36(s) set_fg_color(6) end
+
def on_37(s) set_fg_color(7) end
+
def on_38(s) set_fg_color_256(s) end
+
def on_39(s) set_fg_color(9) end
def on_40(s) set_bg_color(0) end
+
def on_41(s) set_bg_color(1) end
+
def on_42(s) set_bg_color(2) end
+
def on_43(s) set_bg_color(3) end
+
def on_44(s) set_bg_color(4) end
+
def on_45(s) set_bg_color(5) end
+
def on_46(s) set_bg_color(6) end
+
def on_47(s) set_bg_color(7) end
+
def on_48(s) set_bg_color_256(s) end
+
def on_49(s) set_bg_color(9) end
def on_90(s) set_fg_color(0, 'l') end
+
def on_91(s) set_fg_color(1, 'l') end
+
def on_92(s) set_fg_color(2, 'l') end
+
def on_93(s) set_fg_color(3, 'l') end
+
def on_94(s) set_fg_color(4, 'l') end
+
def on_95(s) set_fg_color(5, 'l') end
+
def on_96(s) set_fg_color(6, 'l') end
+
def on_97(s) set_fg_color(7, 'l') end
+
def on_99(s) set_fg_color(9, 'l') end
def on_100(s) set_bg_color(0, 'l') end
+
def on_101(s) set_bg_color(1, 'l') end
+
def on_102(s) set_bg_color(2, 'l') end
+
def on_103(s) set_bg_color(3, 'l') end
+
def on_104(s) set_bg_color(4, 'l') end
+
def on_105(s) set_bg_color(5, 'l') end
+
def on_106(s) set_bg_color(6, 'l') end
+
def on_107(s) set_bg_color(7, 'l') end
+
def on_109(s) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
- STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
def convert(raw, new_state)
reset_state
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 8b939663ffd..746e76a1b1f 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -24,7 +24,7 @@ module Ci
new_update = current_runner.ensure_runner_queue_value
- result = Ci::RegisterBuildService.new(current_runner).execute
+ result = Ci::RegisterJobService.new(current_runner).execute
if result.valid?
if result.build
@@ -167,7 +167,10 @@ module Ci
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
- build.artifacts_expire_in = params['expire_in']
+ build.artifacts_expire_in =
+ params['expire_in'] ||
+ Gitlab::CurrentSettings.current_application_settings
+ .default_artifacts_expire_in
if build.save
present(build, with: Entities::BuildDetails)
@@ -214,6 +217,7 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
+ status(200)
build.erase_artifacts!
end
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 5ff25a3a9b2..996990b464f 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,7 +1,7 @@
module Ci
module API
module Helpers
- BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+ BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 10 * 60
@@ -60,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
no_content!
else
not_found!
@@ -73,7 +73,7 @@ module Ci
def get_runner_version_from_params
return unless params["info"].present?
- attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ attributes_for_keys(%w(name version revision platform architecture), params["info"])
end
def max_artifacts_size
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index bcc82969eb3..45aa2adccf5 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -1,44 +1,38 @@
module Ci
module API
- # Runners API
class Runners < Grape::API
resource :runners do
- # Delete runner
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # GET /runners/delete
+ desc 'Delete a runner'
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ end
delete "delete" do
- required_attributes! [:token]
authenticate_runner!
+
+ status(200)
Ci::Runner.find_by_token(params[:token]).destroy
end
- # Register a new runner
- #
- # Note: This is an "internal" API called when setting up
- # runners, so it is authenticated differently.
- #
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # POST /runners/register
+ desc 'Register a new runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
+ optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
+ end
post "register" do
- required_attributes! [:token]
-
- attributes = attributes_for_keys(
- [:description, :tag_list, :run_untagged, :locked]
- )
+ runner_params = declared(params, include_missing: false).except(:token)
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(attributes.merge(is_shared: true))
+ Ci::Runner.create(runner_params.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
- project.runners.create(attributes)
+ project.runners.create(runner_params)
end
return forbidden! unless runner
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 649ee4d018b..15a461a16dd 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -1,6 +1,6 @@
module Ci
class GitlabCiYamlProcessor
- class ValidationError < StandardError; end
+ ValidationError = Class.new(StandardError)
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
@@ -58,7 +58,7 @@ module Ci
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
- allow_failure: job[:allow_failure] || false,
+ allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2cbb7bfb67d..196cdd36a88 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -5,7 +5,7 @@ module ContainerRegistry
class Client
attr_accessor :uri
- MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+ MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'.freeze
# Taken from: FaradayMiddleware::FollowRedirects
REDIRECT_CODES = Set.new [301, 302, 303, 307]
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 82551f1f222..dd864eea3fa 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -2,7 +2,7 @@
# file path string when combined in a request parameter
module ExtractsPath
# Raised when given an invalid file path
- class InvalidPathError < StandardError; end
+ InvalidPathError = Class.new(StandardError)
# Given a string containing both a Git tree-ish, such as a branch or tag, and
# a filesystem path joined by forward slashes, attempts to separate the two.
@@ -42,7 +42,7 @@ module ExtractsPath
return pair unless @project
- if id.match(/^([[:alnum:]]{40})(.+)/)
+ if id =~ /^(\h{40})(.+)/
# If the ref appears to be a SHA, we're done, just split the string
pair = $~.captures
else
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 440dd44ece7..eb19ab45ac3 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -32,9 +32,9 @@ class FileSizeValidator < ActiveModel::EachValidator
end
def validate_each(record, attribute, value)
- raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
+ raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.is_a? CarrierWave::Uploader::Base
- value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)
+ value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.is_a?(String)
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 9b484a2ecfd..8c28009b9c6 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,7 +5,7 @@
#
module Gitlab
module Access
- class AccessDeniedError < StandardError; end
+ AccessDeniedError = Class.new(StandardError)
NO_ACCESS = 0
GUEST = 10
@@ -21,9 +21,7 @@ module Gitlab
PROTECTION_DEV_CAN_MERGE = 3
class << self
- def values
- options.values
- end
+ delegate :values, to: :options
def all_values
options_with_owner.values
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
index f48abcc86d5..e4f7cad2b79 100644
--- a/lib/gitlab/allowable.rb
+++ b/lib/gitlab/allowable.rb
@@ -1,6 +1,6 @@
module Gitlab
module Allowable
- def can?(user, action, subject)
+ def can?(user, action, subject = :global)
Ability.allowed?(user, action, subject)
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index f638905a1e0..eee5601b0ed 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,10 +1,18 @@
module Gitlab
module Auth
- class MissingPersonalTokenError < StandardError; end
+ MissingPersonalTokenError = Class.new(StandardError)
- SCOPES = [:api, :read_user]
- DEFAULT_SCOPES = [:api]
- OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+ # Scopes used for GitLab API access
+ API_SCOPES = [:api, :read_user].freeze
+
+ # Scopes used for OpenID Connect
+ OPENID_SCOPES = [:openid].freeze
+
+ # Default scopes for OAuth applications that don't define their own
+ DEFAULT_SCOPES = [:api].freeze
+
+ # Other available scopes
+ OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
def find_for_git_client(login, password, project:, ip:)
@@ -18,27 +26,30 @@ module Gitlab
build_access_token_check(login, password) ||
lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
- personal_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
+ personal_access_token_check(password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
result
end
def find_with_user_password(login, password)
- user = User.by_login(login)
+ Gitlab::Auth::UniqueIpsLimiter.limit_user! do
+ user = User.by_login(login)
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- return nil unless Gitlab::LDAP::Config.enabled?
+ # If no user is found, or it's an LDAP server, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ if user.nil? || user.ldap_user?
+ # Second chance - try LDAP authentication
+ return nil unless Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Authentication.login(login, password)
- else
- user if user.valid_password?(password)
+ Gitlab::LDAP::Authentication.login(login, password)
+ else
+ user if user.active? && user.valid_password?(password)
+ end
end
end
@@ -102,14 +113,13 @@ module Gitlab
end
end
- def personal_access_token_check(login, password)
- if login && password
- token = PersonalAccessToken.active.find_by_token(password)
- validation = User.by_login(login)
+ def personal_access_token_check(password)
+ return unless password.present?
- if valid_personal_access_token?(token, validation)
- Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
- end
+ token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
+
+ if token && valid_api_token?(token)
+ Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
end
end
@@ -117,10 +127,6 @@ module Gitlab
token && token.accessible? && valid_api_token?(token)
end
- def valid_personal_access_token?(token, user)
- token && token.user == user && valid_api_token?(token)
- end
-
def valid_api_token?(token)
AccessTokenValidationService.new(token).include_any_scope?(['api'])
end
diff --git a/lib/gitlab/auth/too_many_ips.rb b/lib/gitlab/auth/too_many_ips.rb
new file mode 100644
index 00000000000..ed862791551
--- /dev/null
+++ b/lib/gitlab/auth/too_many_ips.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Auth
+ class TooManyIps < StandardError
+ attr_reader :user_id, :ip, :unique_ips_count
+
+ def initialize(user_id, ip, unique_ips_count)
+ @user_id = user_id
+ @ip = ip
+ @unique_ips_count = unique_ips_count
+ end
+
+ def message
+ "User #{user_id} from IP: #{ip} tried logging from too many ips: #{unique_ips_count}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
new file mode 100644
index 00000000000..bf2239ca150
--- /dev/null
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Auth
+ class UniqueIpsLimiter
+ USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze
+
+ class << self
+ def limit_user_id!(user_id)
+ if config.unique_ips_limit_enabled
+ ip = RequestContext.client_ip
+ unique_ips = update_and_return_ips_count(user_id, ip)
+
+ raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
+ end
+ end
+
+ def limit_user!(user = nil)
+ user ||= yield if block_given?
+ limit_user_id!(user.id) unless user.nil?
+ user
+ end
+
+ def config
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def update_and_return_ips_count(user_id, ip)
+ time = Time.now.utc.to_i
+ key = "#{USER_UNIQUE_IPS_PREFIX}:#{user_id}"
+
+ Gitlab::Redis.with do |redis|
+ unique_ips_count = nil
+ redis.multi do |r|
+ r.zadd(key, time, ip)
+ r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window)
+ unique_ips_count = r.zcard(key)
+ end
+ unique_ips_count.value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
deleted file mode 100644
index 39b43ab5489..00000000000
--- a/lib/gitlab/award_emoji.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module Gitlab
- class AwardEmoji
- CATEGORIES = {
- objects: "Objects",
- travel: "Travel",
- symbols: "Symbols",
- nature: "Nature",
- people: "People",
- activity: "Activity",
- flags: "Flags",
- food: "Food"
- }.with_indifferent_access
-
- def self.normalize_emoji_name(name)
- aliases[name] || name
- end
-
- def self.emoji_by_category
- unless @emoji_by_category
- @emoji_by_category = Hash.new { |h, key| h[key] = [] }
-
- emojis.each do |emoji_name, data|
- data["name"] = emoji_name
-
- # Skip Fitzpatrick(tone) modifiers
- next if data["category"] == "modifier"
-
- category = data["category"]
-
- @emoji_by_category[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
-
- # Returns an Array of Emoji names and their asset URLs.
- def self.urls
- @urls ||= begin
- path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
- # Construct the full asset path ourselves because
- # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
- # of entries since it has to do a lot of extra work (e.g. regexps).
- prefix = Gitlab::Application.config.assets.prefix
- digest = Gitlab::Application.config.assets.digest
- base =
- if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
- Gitlab::Application.config.relative_url_root
- else
- ''
- end
-
- JSON.parse(File.read(path)).map do |hash|
- if digest
- fname = "#{hash['unicode']}-#{hash['digest']}"
- else
- fname = hash['unicode']
- end
-
- { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index 2b95ddfcb53..bc0e0cd441d 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -15,7 +15,7 @@ module Gitlab
canceled: '#9f9f9f',
skipped: '#9f9f9f',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index 06e0d084e9f..fcecb1d9665 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -13,7 +13,7 @@ module Gitlab
medium: '#dfb317',
low: '#e05d44',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 95308aca95f..5b32fca00a4 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_reader :raw_changes
def initialize(changes)
- @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+ @raw_changes = changes.is_a?(String) ? changes.lines : changes
end
def each(&block)
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
index a0058407fb2..054f7f4be0c 100644
--- a/lib/gitlab/chat_commands/presenters/issue_base.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -32,7 +32,7 @@ module Gitlab
},
{
title: "Labels",
- value: @resource.labels.any? ? @resource.label_names : "_None_",
+ value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
short: true
}
]
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 273118135a9..c85f79127bc 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,16 +1,20 @@
module Gitlab
module Checks
class ChangeAccess
- attr_reader :user_access, :project, :skip_authorization
+ # protocol is currently used only in EE
+ attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
- change, user_access:, project:, env: {}, skip_authorization: false)
+ change, user_access:, project:, env: {}, skip_authorization: false,
+ protocol:
+ )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
@skip_authorization = skip_authorization
+ @protocol = protocol
end
def exec
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index cd2e83b4c27..a375ccbece0 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -6,7 +6,7 @@ module Gitlab
module Build
module Artifacts
class Metadata
- class ParserError < StandardError; end
+ ParserError = Class.new(StandardError)
VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 7f4c750b6fd..6f799c2f031 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -27,6 +27,8 @@ module Gitlab
end
end
+ delegate :empty?, to: :children
+
def directory?
blank_node? || @path.end_with?('/')
end
@@ -91,10 +93,6 @@ module Gitlab
blank_node? || @entries.include?(@path)
end
- def empty?
- children.empty?
- end
-
def total_size
descendant_pattern = %r{^#{Regexp.escape(@path)}}
entries.sum do |path, entry|
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
new file mode 100644
index 00000000000..c62aeb60fa9
--- /dev/null
+++ b/lib/gitlab/ci/build/image.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Build
+ class Image
+ attr_reader :name
+
+ class << self
+ def from_image(job)
+ image = Gitlab::Ci::Build::Image.new(job.options[:image])
+ return unless image.valid?
+ image
+ end
+
+ def from_services(job)
+ services = job.options[:services].to_a.map do |service|
+ Gitlab::Ci::Build::Image.new(service)
+ end
+
+ services.select(&:valid?).compact
+ end
+ end
+
+ def initialize(image)
+ @name = image
+ end
+
+ def valid?
+ @name.present?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
new file mode 100644
index 00000000000..1877429ac46
--- /dev/null
+++ b/lib/gitlab/ci/build/step.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module Ci
+ module Build
+ class Step
+ WHEN_ON_FAILURE = 'on_failure'.freeze
+ WHEN_ON_SUCCESS = 'on_success'.freeze
+ WHEN_ALWAYS = 'always'.freeze
+
+ attr_reader :name
+ attr_writer :script
+ attr_accessor :timeout, :when, :allow_failure
+
+ class << self
+ def from_commands(job)
+ self.new(:script).tap do |step|
+ step.script = job.commands
+ step.timeout = job.timeout
+ step.when = WHEN_ON_SUCCESS
+ end
+ end
+
+ def from_after_script(job)
+ after_script = job.options[:after_script]
+ return unless after_script
+
+ self.new(:after_script).tap do |step|
+ step.script = after_script
+ step.timeout = job.timeout
+ step.when = WHEN_ALWAYS
+ step.allow_failure = true
+ end
+ end
+ end
+
+ def initialize(name)
+ @name = name
+ @allow_failure = false
+ end
+
+ def script
+ @script.split("\n")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index b756b0d4555..8275aacee9b 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -9,7 +9,7 @@ module Gitlab
include Validatable
include Attributable
- ALLOWED_KEYS = %i[name untracked paths when expire_in]
+ ALLOWED_KEYS = %i[name untracked paths when expire_in].freeze
attributes ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 7653cab668b..f074df9c7a1 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -8,7 +8,7 @@ module Gitlab
class Cache < Node
include Configurable
- ALLOWED_KEYS = %i[key untracked paths]
+ ALLOWED_KEYS = %i[key untracked paths].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -22,6 +22,12 @@ module Gitlab
entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
+
+ helpers :key
+
+ def value
+ super.merge(key: key_value)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index f7c530c7d9f..0c1f9eb7cbf 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
class Environment < Node
include Validatable
- ALLOWED_KEYS = %i[name url action on_stop]
+ ALLOWED_KEYS = %i[name url action on_stop].freeze
validations do
validate do
@@ -21,12 +21,14 @@ module Gitlab
validates :name,
type: {
with: String,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
validates :name,
format: {
with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
with_options if: :hash? do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 9f5e393d191..6be8288748f 100644
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -6,7 +6,7 @@ module Gitlab
# Factory class responsible for fabricating entry objects.
#
class Factory
- class InvalidFactory < StandardError; end
+ InvalidFactory = Class.new(StandardError)
def initialize(entry)
@entry = entry
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 69a5e6f433d..176301bcca1 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
- after_script variables environment coverage]
+ after_script variables environment coverage].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -104,6 +104,14 @@ module Gitlab
(before_script_value.to_a + script_value.to_a).join("\n")
end
+ def manual_action?
+ self.when == 'manual'
+ end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
+
private
def inherit!(deps)
@@ -135,7 +143,8 @@ module Gitlab
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
- after_script: after_script_value }
+ after_script: after_script_value,
+ ignore: ignored? }
end
end
end
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0e4c9fe6edc..f27ad0a7759 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -11,6 +11,10 @@ module Gitlab
validations do
validates :config, key: true
end
+
+ def self.default
+ 'default'
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index 5eef2868cd6..a6a914d79c1 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -6,7 +6,7 @@ module Gitlab
# Base abstract class for each configuration entry node.
#
class Node
- class InvalidError < StandardError; end
+ InvalidError = Class.new(StandardError)
attr_reader :config, :metadata
attr_accessor :key, :parent, :description
@@ -70,6 +70,12 @@ module Gitlab
true
end
+ def inspect
+ val = leaf? ? config : descendants
+ unspecified = specified? ? '' : '(unspecified) '
+ "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
+ end
+
def self.default
end
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index b33b8238230..1171ac10f22 100644
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -29,6 +29,10 @@ module Gitlab
def relevant?
false
end
+
+ def inspect
+ "#<#{self.class.name}>"
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
index dbf6eb0edbe..e7d9f6a7761 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/ci/config/loader.rb
@@ -2,7 +2,7 @@ module Gitlab
module Ci
class Config
class Loader
- class FormatError < StandardError; end
+ FormatError = Class.new(StandardError)
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 0f4b7b24cef..3495b8d0448 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -5,22 +5,10 @@ module Gitlab
class Play < SimpleDelegator
include Status::Extended
- def text
- 'manual'
- end
-
def label
'manual play action'
end
- def icon
- 'icon_status_manual'
- end
-
- def group
- 'manual'
- end
-
def has_action?
can?(user, :update_build, subject)
end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index 90401cad0d2..e8530f2aaae 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -5,22 +5,10 @@ module Gitlab
class Stop < SimpleDelegator
include Status::Extended
- def text
- 'manual'
- end
-
def label
'manual stop action'
end
- def icon
- 'icon_status_manual'
- end
-
- def group
- 'manual'
- end
-
def has_action?
can?(user, :update_build, subject)
end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
new file mode 100644
index 00000000000..5f28521901d
--- /dev/null
+++ b/lib/gitlab/ci/status/manual.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ class Manual < Status::Core
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
new file mode 100644
index 00000000000..a250c3fcb41
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ class Blocked < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'blocked'
+ end
+
+ def label
+ 'waiting for manual action'
+ end
+
+ def self.matches?(pipeline, user)
+ pipeline.blocked?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 13c8343b12a..17f9a75f436 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -4,7 +4,8 @@ module Gitlab
module Pipeline
class Factory < Status::Factory
def self.extended_statuses
- [Status::SuccessWarning]
+ [[Status::SuccessWarning,
+ Status::Pipeline::Blocked]]
end
def self.common_helpers
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index c843315782d..75a213ef752 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -4,8 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers
include IconsHelper
- class MissingResolution < ResolutionError
- end
+ MissingResolution = Class.new(ResolutionError)
CONTEXT_LINES = 3
@@ -91,11 +90,12 @@ module Gitlab
our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
lines.each do |line|
- if line.type == 'old'
- line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
- else
- line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
- end
+ line.rich_text =
+ if line.type == 'old'
+ their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ our_highlight[line.new_line - 1].try(:html_safe)
+ end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index fa5bd4649d4..990b719ecfd 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -1,8 +1,7 @@
module Gitlab
module Conflict
class FileCollection
- class ConflictSideMissing < StandardError
- end
+ ConflictSideMissing = Class.new(StandardError)
attr_reader :merge_request, :our_commit, :their_commit
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index ddd657903fb..84f9ecd3d23 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -1,35 +1,23 @@
module Gitlab
module Conflict
class Parser
- class UnresolvableError < StandardError
- end
-
- class UnmergeableFile < UnresolvableError
- end
-
- class UnsupportedEncoding < UnresolvableError
- end
+ UnresolvableError = Class.new(StandardError)
+ UnmergeableFile = Class.new(UnresolvableError)
+ UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
- class ParserError < StandardError
- end
-
- class UnexpectedDelimiter < ParserError
- end
-
- class MissingEndDelimiter < ParserError
- end
+ ParserError = Class.new(StandardError)
+ UnexpectedDelimiter = Class.new(ParserError)
+ MissingEndDelimiter = Class.new(ParserError)
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
- begin
- text.to_json
- rescue Encoding::UndefinedConversionError
- raise UnsupportedEncoding
- end
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
line_obj_index = 0
line_old = 1
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
index a0f2006bc24..0b61256b35a 100644
--- a/lib/gitlab/conflict/resolution_error.rb
+++ b/lib/gitlab/conflict/resolution_error.rb
@@ -1,6 +1,5 @@
module Gitlab
module Conflict
- class ResolutionError < StandardError
- end
+ ResolutionError = Class.new(StandardError)
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index e20f5f6f514..82576d197fe 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -25,9 +25,7 @@ module Gitlab
settings || in_memory_application_settings
end
- def sidekiq_throttling_enabled?
- current_application_settings.sidekiq_throttling_enabled?
- end
+ delegate :sidekiq_throttling_enabled?, to: :current_application_settings
def in_memory_application_settings
@in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index e50e54b6e99..182a30fd74d 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -39,7 +39,7 @@ module Gitlab
started_at: build.started_at,
finished_at: build.finished_at,
when: build.when,
- manual: build.manual?,
+ manual: build.action?,
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a47d7e98a62..63b8d0d3b9d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ def self.config
+ ActiveRecord::Base.configurations[Rails.env]
+ end
+
def self.adapter_name
- ActiveRecord::Base.configurations[Rails.env]['adapter']
+ config['adapter']
end
def self.mysql?
@@ -24,7 +28,7 @@ module Gitlab
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
- if Gitlab::Database.postgresql?
+ if postgresql?
order << ' NULLS LAST'
else
# `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -38,7 +42,7 @@ module Gitlab
def self.nulls_first_order(field, direction = 'ASC')
order = "#{field} #{direction}"
- if Gitlab::Database.postgresql?
+ if postgresql?
order << ' NULLS FIRST'
else
# `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
@@ -50,7 +54,7 @@ module Gitlab
end
def self.random
- Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ postgresql? ? "RANDOM()" : "RAND()"
end
def true_value
@@ -79,11 +83,16 @@ module Gitlab
end
end
- def self.create_connection_pool(pool_size)
+ # pool_size - The size of the DB pool.
+ # host - An optional host name to use instead of the default one.
+ def self.create_connection_pool(pool_size, host = nil)
# See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb
env = Rails.env
original_config = ActiveRecord::Base.configurations
+
env_config = original_config[env].merge('pool' => pool_size)
+ env_config['host'] = host if host
+
config = original_config.merge(env => env_config)
spec =
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 08607c27c09..23890e5f493 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -108,6 +108,7 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
end
+
# Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 9ea976e18fa..7db896522a9 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -50,7 +50,7 @@ module Gitlab
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
if rich_line
- line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+ line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' '
"#{line_prefix}#{rich_line}".html_safe
end
end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 87a9b1e23ac..736933b1c4b 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -4,7 +4,7 @@ module Gitlab
MARKDOWN_SYMBOLS = {
addition: "+",
deletion: "-"
- }
+ }.freeze
attr_accessor :raw_line, :rich_line
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 80a146b4a5a..114656958e3 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -38,11 +38,11 @@ module Gitlab
end
def added?
- type == 'new'
+ type == 'new' || type == 'new-nonewline'
end
def removed?
- type == 'old'
+ type == 'old' || type == 'old-nonewline'
end
def rich_text
@@ -52,7 +52,7 @@ module Gitlab
end
def meta?
- type == 'match' || type == 'nonewline'
+ type == 'match'
end
def as_json(opts = nil)
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 89320f5d9dc..742f989c50b 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -11,6 +11,7 @@ module Gitlab
line_old = 1
line_new = 1
type = nil
+ context = nil
# By returning an Enumerator we make it possible to search for a single line (with #find)
# without having to instantiate all the others that come after it.
@@ -20,7 +21,7 @@ module Gitlab
full_line = line.delete("\n")
- if line.match(/^@@ -/)
+ if line =~ /^@@ -/
type = "match"
line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
@@ -31,7 +32,8 @@ module Gitlab
line_obj_index += 1
next
elsif line[0] == '\\'
- type = 'nonewline'
+ type = "#{context}-nonewline"
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
else
@@ -43,8 +45,10 @@ module Gitlab
case line[0]
when "+"
line_new += 1
+ context = :new
when "-"
line_old += 1
+ context = :old
when "\\" # rubocop:disable Lint/EmptyWhen
# No increment
else
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index ecf62dead35..fc728123c97 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -140,15 +140,16 @@ module Gitlab
def find_diff_file(repository)
# We're at the initial commit, so just get that as we can't compare to anything.
- if Gitlab::Git.blank_ref?(start_sha)
- compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
- else
- compare = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- )
- end
+ compare =
+ if Gitlab::Git.blank_ref?(start_sha)
+ Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
diff = compare.diffs(paths: paths).first
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 40a4815a9a0..543e62794c5 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -3,8 +3,8 @@ module Gitlab
class Message
attr_reader :path, :offline
- OFFLINE = "\e[31moffline\e[0m"
- ONLINE = "\e[32monline\e[0m"
+ OFFLINE = "\e[31moffline\e[0m".freeze
+ ONLINE = "\e[32monline\e[0m".freeze
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c8e36d8ff4a..e0fdf3f3d64 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -119,7 +119,7 @@ module Gitlab
step("Reseting to latest master", %w[git reset --hard origin/master])
step("Checking if #{patch_path} applies cleanly to EE/master")
- output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
+ output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}])
unless status.zero?
failed_files = output.lines.reduce([]) do |memo, line|
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd2f5d3615e..35ea2e0ef59 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -5,7 +5,7 @@ require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
+ HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index b64db5d01ae..ec0529b5a4b 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -4,19 +4,19 @@ require_dependency 'gitlab/email/handler'
# Inspired in great part by Discourse's Email::Receiver
module Gitlab
module Email
- class ProcessingError < StandardError; end
- class EmailUnparsableError < ProcessingError; end
- class SentNotificationNotFoundError < ProcessingError; end
- class ProjectNotFound < ProcessingError; end
- class EmptyEmailError < ProcessingError; end
- class AutoGeneratedEmailError < ProcessingError; end
- class UserNotFoundError < ProcessingError; end
- class UserBlockedError < ProcessingError; end
- class UserNotAuthorizedError < ProcessingError; end
- class NoteableNotFoundError < ProcessingError; end
- class InvalidNoteError < ProcessingError; end
- class InvalidIssueError < ProcessingError; end
- class UnknownIncomingEmail < ProcessingError; end
+ ProcessingError = Class.new(StandardError)
+ EmailUnparsableError = Class.new(ProcessingError)
+ SentNotificationNotFoundError = Class.new(ProcessingError)
+ ProjectNotFound = Class.new(ProcessingError)
+ EmptyEmailError = Class.new(ProcessingError)
+ AutoGeneratedEmailError = Class.new(ProcessingError)
+ UserNotFoundError = Class.new(ProcessingError)
+ UserBlockedError = Class.new(ProcessingError)
+ UserNotAuthorizedError = Class.new(ProcessingError)
+ NoteableNotFoundError = Class.new(ProcessingError)
+ InvalidNoteError = Class.new(ProcessingError)
+ InvalidIssueError = Class.new(ProcessingError)
+ UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver
def initialize(raw)
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 8c8dd1b9cef..558df87f36d 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -31,11 +31,12 @@ module Gitlab
private
def select_body(message)
- if message.multipart?
- part = message.text_part || message.html_part || message
- else
- part = message
- end
+ part =
+ if message.multipart?
+ message.text_part || message.html_part || message
+ else
+ message
+ end
decoded = fix_charset(part)
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index bbbca8acc40..a16d9fc2265 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -1,7 +1,7 @@
module Gitlab
module Emoji
extend self
-
+
def emojis
Gemojione.index.instance_variable_get(:@emoji_by_name)
end
@@ -18,6 +18,10 @@ module Gitlab
emojis.keys
end
+ def emojis_aliases
+ @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
+ end
+
def emoji_filename(name)
emojis[name]["unicode"]
end
@@ -25,5 +29,32 @@ module Gitlab
def emoji_unicode_filename(moji)
emojis_by_moji[moji]["unicode"]
end
+
+ def emoji_unicode_version(name)
+ @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ @emoji_unicode_versions_by_name[name]
+ end
+
+ def normalize_emoji_name(name)
+ emojis_aliases[name] || name
+ end
+
+ def emoji_image_tag(name, src)
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
+ end
+
+ # CSS sprite fallback takes precedence over image fallback
+ def gl_emoji_tag(name)
+ emoji_name = emojis_aliases[name] || name
+ emoji_info = emojis[emoji_name]
+ return unless emoji_info
+
+ data = {
+ name: emoji_name,
+ unicode_version: emoji_unicode_version(emoji_name)
+ }
+
+ ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], data: data)
+ end
end
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
new file mode 100644
index 00000000000..ffbc6e17dc5
--- /dev/null
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module EtagCaching
+ class Middleware
+ RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
+ ROUTE_REGEXP = Regexp.union(
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+ )
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless enabled_for_current_route?(env)
+ Gitlab::Metrics.add_event(:etag_caching_middleware_used)
+
+ etag, cached_value_present = get_etag(env)
+ if_none_match = env['HTTP_IF_NONE_MATCH']
+
+ if if_none_match == etag
+ Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+ [304, { 'ETag' => etag }, ['']]
+ else
+ track_cache_miss(if_none_match, cached_value_present)
+
+ status, headers, body = @app.call(env)
+ headers['ETag'] = etag
+ [status, headers, body]
+ end
+ end
+
+ private
+
+ def enabled_for_current_route?(env)
+ ROUTE_REGEXP.match(env['PATH_INFO'])
+ end
+
+ def get_etag(env)
+ cache_key = env['PATH_INFO']
+ store = Gitlab::EtagCaching::Store.new
+ current_value = store.get(cache_key)
+ cached_value_present = current_value.present?
+
+ unless cached_value_present
+ current_value = store.touch(cache_key, only_if_missing: true)
+ end
+
+ [weak_etag_format(current_value), cached_value_present]
+ end
+
+ def weak_etag_format(value)
+ %Q{W/"#{value}"}
+ end
+
+ def track_cache_miss(if_none_match, cached_value_present)
+ if if_none_match.blank?
+ Gitlab::Metrics.add_event(:etag_caching_header_missing)
+ elsif !cached_value_present
+ Gitlab::Metrics.add_event(:etag_caching_key_not_found)
+ else
+ Gitlab::Metrics.add_event(:etag_caching_resource_changed)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
new file mode 100644
index 00000000000..9532e432f78
--- /dev/null
+++ b/lib/gitlab/etag_caching/store.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module EtagCaching
+ class Store
+ EXPIRY_TIME = 10.minutes
+ REDIS_NAMESPACE = 'etag:'.freeze
+
+ def get(key)
+ Gitlab::Redis.with { |redis| redis.get(redis_key(key)) }
+ end
+
+ def touch(key, only_if_missing: false)
+ etag = generate_etag
+
+ Gitlab::Redis.with do |redis|
+ redis.set(redis_key(key), etag, ex: EXPIRY_TIME, nx: only_if_missing)
+ end
+
+ etag
+ end
+
+ private
+
+ def generate_etag
+ SecureRandom.hex
+ end
+
+ def redis_key(key)
+ "#{REDIS_NAMESPACE}#{key}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2dd42704396..62ddd45785d 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -10,7 +10,7 @@ module Gitlab
# ExclusiveLease.
#
class ExclusiveLease
- LUA_CANCEL_SCRIPT = <<-EOS
+ LUA_CANCEL_SCRIPT = <<-EOS.freeze
local key, uuid = KEYS[1], ARGV[1]
if redis.call("get", key) == uuid then
redis.call("del", key)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 1d93a67dc56..c9ca4cadd1c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
- }
+ }.freeze
# Returns an Array of file types based on the given paths.
#
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b742d9e1e4b..e56eb0d3beb 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -93,163 +93,6 @@ module Gitlab
commit_id: sha,
)
end
-
- # Commit file in repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # content: 'Lorem ipsum...',
- # path: 'documents/story.txt',
- # update: true
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Wow such commit',
- # branch: 'master',
- # update_ref: false
- # }
- #
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def commit(repository, options, action = :add)
- file = options[:file]
- update = file[:update].nil? ? true : file[:update]
- author = options[:author]
- committer = options[:committer]
- commit = options[:commit]
- repo = repository.rugged
- ref = commit[:branch]
- update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
- parents = []
- mode = 0o100644
-
- unless ref.start_with?('refs/')
- ref = 'refs/heads/' + ref
- end
-
- path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
- # Abort if any invalid characters remain (e.g. ../foo)
- raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
-
- filename = path_name.to_s
- index = repo.index
-
- unless repo.empty?
- rugged_ref = repo.references[ref]
- raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
- last_commit = rugged_ref.target
- index.read_tree(last_commit.tree)
- parents = [last_commit]
- end
-
- if action == :remove
- index.remove(filename)
- else
- file_entry = index.get(filename)
-
- if action == :rename
- old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
- old_filename = old_path_name.to_s
- file_entry = index.get(old_filename)
- index.remove(old_filename) unless file_entry.blank?
- end
-
- if file_entry
- raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
-
- # Preserve the current file mode if one is available
- mode = file_entry[:mode] if file_entry[:mode]
- end
-
- content = file[:content]
- detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
-
- unless detect && detect[:type] == :binary
- # When writing to the repo directly as we are doing here,
- # the `core.autocrlf` config isn't taken into account.
- content.gsub!("\r\n", "\n") if repository.autocrlf
- end
-
- oid = repo.write(content, :blob)
- index.add(path: filename, oid: oid, mode: mode)
- end
-
- opts = {}
- opts[:tree] = index.write_tree(repo)
- opts[:author] = author
- opts[:committer] = committer
- opts[:message] = commit[:message]
- opts[:parents] = parents
- opts[:update_ref] = ref if update_ref
-
- Rugged::Commit.create(repo, opts)
- end
- # rubocop:enable Metrics/AbcSize
- # rubocop:enable Metrics/CyclomaticComplexity
- # rubocop:enable Metrics/PerceivedComplexity
-
- # Remove file from repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # path: 'documents/story.txt'
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Remove FILENAME',
- # branch: 'master'
- # }
- #
- def remove(repository, options)
- commit(repository, options, :remove)
- end
-
- # Rename file from repository and return commit sha
- #
- # options should contain next structure:
- # file: {
- # previous_path: 'documents/old_story.txt'
- # path: 'documents/story.txt'
- # content: 'Lorem ipsum...',
- # update: true
- # },
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Rename FILENAME',
- # branch: 'master'
- # }
- #
- def rename(repository, options)
- commit(repository, options, :rename)
- end
end
def initialize(options)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index d785516ebdd..3a73697dc5d 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,6 +14,8 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+ delegate :tree, to: :raw_commit
+
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -218,10 +220,6 @@ module Gitlab
raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
end
- def tree
- raw_commit.tree
- end
-
def stats
Gitlab::Git::CommitStats.new(self)
end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index d6b3b5705a9..019be151353 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -2,7 +2,7 @@
module Gitlab
module Git
class Diff
- class TimeoutError < StandardError; end
+ TimeoutError = Class.new(StandardError)
include Gitlab::Git::EncodingHelper
# Diff properties
@@ -176,9 +176,13 @@ module Gitlab
def initialize(raw_diff, collapse: false)
case raw_diff
when Hash
- init_from_hash(raw_diff, collapse: collapse)
+ init_from_hash(raw_diff)
+ prune_diff_if_eligible(collapse)
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff, collapse: collapse)
+ when Gitaly::CommitDiffResponse
+ init_from_gitaly(raw_diff)
+ prune_diff_if_eligible(collapse)
when nil
raise "Nil as raw diff passed"
else
@@ -266,13 +270,26 @@ module Gitlab
@diff = encode!(strip_diff_headers(patch.to_s))
end
- def init_from_hash(hash, collapse: false)
+ def init_from_hash(hash)
raw_diff = hash.symbolize_keys
serialize_keys.each do |key|
send(:"#{key}=", raw_diff[key.to_sym])
end
+ end
+
+ def init_from_gitaly(diff_msg)
+ @diff = diff_msg.raw_chunks.join
+ @new_path = encode!(diff_msg.to_path.dup)
+ @old_path = encode!(diff_msg.from_path.dup)
+ @a_mode = diff_msg.old_mode.to_s(8)
+ @b_mode = diff_msg.new_mode.to_s(8)
+ @new_file = diff_msg.from_id == BLANK_SHA
+ @renamed_file = diff_msg.from_path != diff_msg.to_path
+ @deleted_file = diff_msg.to_id == BLANK_SHA
+ end
+ def prune_diff_if_eligible(collapse = false)
prune_large_diff! if too_large?
prune_collapsed_diff! if collapse && collapsible?
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065d..4e45ec7c174 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
elsif @deltas_only
each_delta(&block)
else
- each_patch(&block)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+ each_patch(&block)
+ end
end
end
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
new file mode 100644
index 00000000000..af1744c9c46
--- /dev/null
+++ b/lib/gitlab/git/index.rb
@@ -0,0 +1,126 @@
+module Gitlab
+ module Git
+ class Index
+ DEFAULT_MODE = 0o100644
+
+ attr_reader :repository, :raw_index
+
+ def initialize(repository)
+ @repository = repository
+ @raw_index = repository.rugged.index
+ end
+
+ delegate :read_tree, :get, to: :raw_index
+
+ def write_tree
+ raw_index.write_tree(repository.rugged)
+ end
+
+ def dir_exists?(path)
+ raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
+ end
+
+ def create(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
+ end
+
+ add_blob(options)
+ end
+
+ def create_dir(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
+ end
+
+ if dir_exists?(options[:file_path])
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
+ end
+
+ options = options.dup
+ options[:file_path] += '/.gitkeep'
+ options[:content] = ''
+
+ add_blob(options)
+ end
+
+ def update(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def move(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:previous_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:previous_path])
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def delete(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:file_path])
+ end
+
+ private
+
+ def normalize_options(options)
+ options = options.dup
+ options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
+ options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
+ options
+ end
+
+ def normalize_path(path)
+ pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
+
+ if pathname.each_filename.include?('..')
+ raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ end
+
+ pathname.to_s
+ end
+
+ def add_blob(options, mode: nil)
+ content = options[:content]
+ content = Base64.decode64(content) if options[:encoding] == 'base64'
+
+ detect = CharlockHolmes::EncodingDetector.new.detect(content)
+ unless detect && detect[:type] == :binary
+ # When writing to the repo directly as we are doing here,
+ # the `core.autocrlf` config isn't taken into account.
+ content.gsub!("\r\n", "\n") if repository.autocrlf
+ end
+
+ oid = repository.rugged.write(content, :blob)
+
+ raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
+ rescue Rugged::IndexError => e
+ raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 7068e68a855..2187dd70ff4 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,5 +1,4 @@
# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
-require 'forwardable'
require 'tempfile'
require 'forwardable'
require "rubygems/package"
@@ -7,14 +6,13 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
- extend Forwardable
include Gitlab::Git::Popen
SEARCH_CONTEXT_LINES = 3
- class NoRepository < StandardError; end
- class InvalidBlobName < StandardError; end
- class InvalidRef < StandardError; end
+ NoRepository = Class.new(StandardError)
+ InvalidBlobName = Class.new(StandardError)
+ InvalidRef = Class.new(StandardError)
# Full path to repo
attr_reader :path
@@ -33,6 +31,10 @@ module Gitlab
@attributes = Gitlab::Git::Attributes.new(path)
end
+ delegate :empty?,
+ :bare?,
+ to: :rugged
+
# Default branch in the repository
def root_ref
@root_ref ||= discover_default_branch
@@ -162,14 +164,6 @@ module Gitlab
!empty?
end
- def empty?
- rugged.empty?
- end
-
- def bare?
- rugged.bare?
- end
-
def repo_exists?
!!rugged
end
@@ -205,13 +199,17 @@ module Gitlab
nil
end
+ def archive_prefix(ref, sha)
+ project_name = self.name.chomp('.git')
+ "#{project_name}-#{ref.tr('/', '-')}-#{sha}"
+ end
+
def archive_metadata(ref, storage_path, format = "tar.gz")
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
- project_name = self.name.chomp('.git')
- prefix = "#{project_name}-#{ref}-#{commit.id}"
+ prefix = archive_prefix(ref, commit.id)
{
'RepoPath' => path,
@@ -330,24 +328,42 @@ module Gitlab
end
def log_by_shell(sha, options)
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
- cmd += %W(-n #{options[:limit].to_i})
- cmd += %w(--format=%H)
- cmd += %W(--skip=#{options[:offset].to_i})
- cmd += %w(--follow) if options[:follow]
- cmd += %w(--no-merges) if options[:skip_merges]
- cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
- cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
- cmd += [sha]
- cmd += %W(-- #{options[:path]}) if options[:path].present?
-
- raw_output = IO.popen(cmd) {|io| io.read }
-
- log = raw_output.lines.map do |c|
- Rugged::Commit.new(rugged, c.strip)
- end
+ limit = options[:limit].to_i
+ offset = options[:offset].to_i
+ use_follow_flag = options[:follow] && options[:path].present?
+
+ # We will perform the offset in Ruby because --follow doesn't play well with --skip.
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+ offset_in_ruby = use_follow_flag && options[:offset].present?
+ limit += offset if offset_in_ruby
+
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
+ cmd << "--max-count=#{limit}"
+ cmd << '--format=%H'
+ cmd << "--skip=#{offset}" unless offset_in_ruby
+ cmd << '--follow' if use_follow_flag
+ cmd << '--no-merges' if options[:skip_merges]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
+ cmd << sha
+ cmd += %W[-- #{options[:path]}] if options[:path].present?
- log.is_a?(Array) ? log : []
+ raw_output = IO.popen(cmd) { |io| io.read }
+ lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
+
+ lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
+ end
+
+ def count_commits(options)
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
+ cmd += %W[--count #{options[:ref]}]
+ cmd += %W[-- #{options[:path]}] if options[:path].present?
+
+ raw_output = IO.popen(cmd) { |io| io.read }
+
+ raw_output.to_i
end
def sha_from_ref(ref)
@@ -565,9 +581,7 @@ module Gitlab
# will trigger a +:mixed+ reset and the working directory will be
# replaced with the content of the index. (Untracked and ignored files
# will be left alone)
- def reset(ref, reset_type)
- rugged.reset(ref, reset_type)
- end
+ delegate :reset, to: :rugged
# Mimic the `git clean` command and recursively delete untracked files.
# Valid keys that can be passed in the +options+ hash are:
@@ -845,57 +859,6 @@ module Gitlab
rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
end
- # Create a new directory with a .gitkeep file. Creates
- # all required nested directories (i.e. mkdir -p behavior)
- #
- # options should contain next structure:
- # author: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # committer: {
- # email: 'user@example.com',
- # name: 'Test User',
- # time: Time.now
- # },
- # commit: {
- # message: 'Wow such commit',
- # branch: 'master',
- # update_ref: false
- # }
- def mkdir(path, options = {})
- # Check if this directory exists; if it does, then don't bother
- # adding .gitkeep file.
- ref = options[:commit][:branch]
- path = Gitlab::Git::PathHelper.normalize_path(path).to_s
- rugged_ref = rugged.ref(ref)
-
- raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
-
- target_commit = rugged_ref.target
-
- raise InvalidRef.new("Invalid target commit") if target_commit.nil?
-
- entry = tree_entry(target_commit, path)
-
- if entry
- if entry[:type] == :blob
- raise InvalidBlobName.new("Directory already exists as a file")
- else
- raise InvalidBlobName.new("Directory already exists")
- end
- end
-
- options[:file] = {
- content: '',
- path: "#{path}/.gitkeep",
- update: true
- }
-
- Gitlab::Git::Blob.commit(self, options)
- end
-
# Returns result like "git ls-files" , recursive and full file path
#
# Ex.
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7e1484613f2..eea2f206902 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -10,10 +10,10 @@ module Gitlab
deploy_key_upload:
'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
- }
+ }.freeze
- DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
- PUSH_COMMANDS = %w{ git-receive-pack }
+ DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
+ PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
@@ -153,7 +153,9 @@ module Gitlab
user_access: user_access,
project: project,
env: @env,
- skip_authorization: deploy_key?).exec
+ skip_authorization: deploy_key?,
+ protocol: protocol
+ ).exec
end
def matching_merge_request?(newrev, branch_name)
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 4d83d8e72a8..0e87ee30c98 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,6 +5,9 @@ module Gitlab
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
+ return false if ref_name.start_with?('refs/heads/')
+ return false if ref_name.start_with?('refs/remotes/')
+
Gitlab::Utils.system_silent(
%W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
new file mode 100644
index 00000000000..5534d4af439
--- /dev/null
+++ b/lib/gitlab/gitaly_client.rb
@@ -0,0 +1,43 @@
+require 'gitaly'
+
+module Gitlab
+ module GitalyClient
+ def self.gitaly_address
+ if Gitlab.config.gitaly.socket_path
+ "unix://#{Gitlab.config.gitaly.socket_path}"
+ end
+ end
+
+ def self.channel
+ return @channel if defined?(@channel)
+
+ @channel =
+ if enabled?
+ # NOTE: Gitaly currently runs on a Unix socket, so permissions are
+ # handled using the file system and no additional authentication is
+ # required (therefore the :this_channel_is_insecure flag)
+ GRPC::Core::Channel.new(gitaly_address, {}, :this_channel_is_insecure)
+ else
+ nil
+ end
+ end
+
+ def self.enabled?
+ gitaly_address.present?
+ end
+
+ def self.feature_enabled?(feature)
+ enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+ end
+
+ def self.migrate(feature)
+ is_enabled = feature_enabled?(feature)
+ metric_name = feature.to_s
+ metric_name += "_gitaly" if is_enabled
+
+ Gitlab::Metrics.measure(metric_name) do
+ yield is_enabled
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 00000000000..525b8d680e9
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class Commit
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ class << self
+ def diff_from_parent(commit, options = {})
+ stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+ repo = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+ parent = commit.parents[0]
+ parent_id = parent ? parent.id : EMPTY_TREE_ID
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repo,
+ left_commit_id: parent_id,
+ right_commit_id: commit.id
+ )
+
+ Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb
new file mode 100644
index 00000000000..b827a56207f
--- /dev/null
+++ b/lib/gitlab/gitaly_client/notifications.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module GitalyClient
+ class Notifications
+ attr_accessor :stub
+
+ def initialize
+ @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+ end
+
+ def post_receive(repo_path)
+ repository = Gitaly::Repository.new(path: repo_path)
+ request = Gitaly::PostReceiveRequest.new(repository: repository)
+ stub.post_receive(request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 0a8d05b5fe1..5d29e698b27 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -18,7 +18,7 @@ module Gitlab
end
def commit_exists?
- project.repository.commit(sha).present?
+ project.repository.branch_names_contains(sha).include?(ref)
end
def short_id
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index d95ff4fd104..eea4a91f17d 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -171,6 +171,8 @@ module Gitlab
end
def clean_up_restored_branches(pull_request)
+ return if pull_request.opened?
+
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
end
@@ -285,7 +287,7 @@ module Gitlab
def fetch_resources(resource_type, *opts)
return if imported?(resource_type)
- opts.last.merge!(page: current_page(resource_type))
+ opts.last[:page] = current_page(resource_type)
client.public_send(resource_type, *opts) do |resources|
yield resources
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
index 29fb0f9d333..27b171d6ddb 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -7,9 +7,7 @@ module Gitlab
raise NotImplementedError
end
- def number
- raw_data.number
- end
+ delegate :number, to: :raw_data
def find_condition
{ iid: number }
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 4ea0200e89b..add7236e339 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -38,7 +38,11 @@ module Gitlab
def source_branch_name
@source_branch_name ||= begin
- source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ if cross_project?
+ "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}"
+ else
+ source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ end
end
end
@@ -52,6 +56,14 @@ module Gitlab
end
end
+ def cross_project?
+ source_branch.repo.id != target_branch.repo.id
+ end
+
+ def opened?
+ state == 'opened'
+ end
+
private
def state
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index b8a5ac907a4..5ab84266b7d 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -1,19 +1,20 @@
module Gitlab
module GonHelper
def add_gon_variables
- gon.api_version = API::API.version
- gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+ gon.api_version = 'v4'
+ gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
+ gon.asset_host = ActionController::Base.asset_host
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- gon.award_menu_url = emojis_path
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
if current_user
gon.current_user_id = current_user.id
gon.current_username = current_user.username
+ gon.current_user_fullname = current_user.name
end
end
end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 9360afedfcb..d787d5db4a0 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def initialize(blob_name, blob_content, repository: nil)
- @formatter = Rouge::Formatters::HTMLGitlab.new
+ @formatter = Rouge::Formatters::HTMLGitlab
@repository = repository
@blob_name = blob_name
@blob_content = blob_content
@@ -28,7 +28,7 @@ module Gitlab
hl_lexer = self.lexer
end
- @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe
+ @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
rescue
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index a46a41bc56e..8b327cfc226 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.6'
+ VERSION = '0.1.6'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
@@ -35,7 +35,7 @@ module Gitlab
end
def export_filename(project:)
- basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.full_path}_#{project.path}"
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
index e341c4d9cf8..788eedf2686 100644
--- a/lib/gitlab/import_export/error.rb
+++ b/lib/gitlab/import_export/error.rb
@@ -1,5 +1,5 @@
module Gitlab
module ImportExport
- class Error < StandardError; end
+ Error = Class.new(StandardError)
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 416194e57d7..ab74c8782f6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,9 @@ excluded_attributes:
- :milestone_id
award_emoji:
- :awardable_id
+ statuses:
+ - :trace
+ - :token
methods:
labels:
@@ -81,6 +84,7 @@ methods:
- :type
statuses:
- :type
+ - :gl_project_id
services:
- :type
merge_request_diff:
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index b79be62245b..3473b466936 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -47,7 +47,13 @@ module Gitlab
def group_members
return [] unless @current_user.can?(:admin_group, @project.group)
- MembersFinder.new(@project.project_members, @project.group).execute(@current_user)
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
+
+ GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index fae792237d9..d44563333a5 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -15,7 +15,7 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
- PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze
@@ -98,12 +98,11 @@ module Gitlab
end
def generate_imported_object
- if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
- trace = @relation_hash.delete('trace')
+ if BUILD_MODELS.include?(@relation_name)
+ @relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
imported_object do |object|
- object.trace = trace
object.commit_id = nil
end
else
@@ -121,7 +120,6 @@ module Gitlab
# project_id may not be part of the export, but we always need to populate it if required.
@relation_hash['project_id'] = project_id
- @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 7084fd1767d..43eb73250b7 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -43,9 +43,7 @@ module Gitlab
attribute_value(:email)
end
- def dn
- entry.dn
- end
+ delegate :dn, to: :entry
private
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index b84c81f1a6c..2d5e47a6f3b 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -1,5 +1,3 @@
-require 'gitlab/o_auth/user'
-
# LDAP extension for User model
#
# * Find or create user from omniauth.auth data
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 4b7a791e497..6aa38542cb4 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -143,11 +143,12 @@ module Gitlab
# signature this would break things. As a result we'll make sure the
# generated method _only_ accepts regular arguments if the underlying
# method also accepts them.
- if method.arity == 0
- args_signature = ''
- else
- args_signature = '*args'
- end
+ args_signature =
+ if method.arity == 0
+ ''
+ else
+ '*args'
+ end
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 47f88727fc8..adc0db1a874 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,8 +2,8 @@ module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
- CONTROLLER_KEY = 'action_controller.instance'
- ENDPOINT_KEY = 'api.endpoint'
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
CONTENT_TYPES = {
'text/html' => :html,
'text/plain' => :txt,
@@ -14,7 +14,7 @@ module Gitlab
'image/jpeg' => :jpeg,
'image/gif' => :gif,
'image/svg+xml' => :svg
- }
+ }.freeze
def initialize(app)
@app = app
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 2e9dd4645e3..d435a33e9c7 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -5,7 +5,7 @@ module Gitlab
class ActionView < ActiveSupport::Subscriber
attach_to :action_view
- SERIES = 'views'
+ SERIES = 'views'.freeze
def render_template(event)
track(event) if current_transaction
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 7bc16181be6..4f9fb1c7853 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -5,7 +5,7 @@ module Gitlab
THREAD_KEY = :_gitlab_metrics_transaction
# The series to store events (e.g. Git pushes) in.
- EVENT_SERIES = 'events'
+ EVENT_SERIES = 'events'.freeze
attr_reader :tags, :values, :method, :metrics
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 5764ab15652..6023fa1820f 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -30,21 +30,69 @@ module Gitlab
end
def go_body(request)
- base_url = Gitlab.config.gitlab.url
- # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
- # We can just ignore the paths and leave the namespace/project
- path_info = request.env["PATH_INFO"]
- path_info.sub!(/^\//, '')
- project_path = path_info.split('/').first(2).join('/')
- request_url = URI.join(base_url, project_path)
- domain_path = strip_url(request_url.to_s)
+ project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+ import_prefix = strip_url(project_url.to_s)
- "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"
+ "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
end
def strip_url(url)
url.gsub(/\Ahttps?:\/\//, '')
end
+
+ def project_path(request)
+ path_info = request.env["PATH_INFO"]
+ path_info.sub!(/^\//, '')
+
+ # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
+ # In a traditional project with a single namespace, this would denote repo
+ # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
+ # groups, this could also be `namespace/project/path1` with subpath
+ # `path2/../pathN`, for example.
+
+ # We find all potential project paths out of the path segments
+ path_segments = path_info.split('/')
+ simple_project_path = path_segments.first(2).join('/')
+
+ # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
+ return simple_project_path if path_segments.length <= 2
+
+ project_paths = []
+ begin
+ project_paths << path_segments.join('/')
+ path_segments.pop
+ end while path_segments.length >= 2
+
+ # We see if a project exists with any of these potential paths
+ project = project_for_paths(project_paths, request)
+
+ if project
+ # If a project is found and the user has access, we return the full project path
+ project.full_path
+ else
+ # If not, we return the first two components as if it were a simple `namespace/project` path,
+ # so that we don't reveal the existence of a nested project the user doesn't have access to.
+ # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
+ # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
+ # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
+ # Since `go get` doesn't authenticate by default, this means that
+ # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
+ # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
+ # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
+ simple_project_path
+ end
+ end
+
+ def project_for_paths(paths, request)
+ project = Project.where_full_path_in(paths).first
+ return unless Ability.allowed?(current_user(request), :read_project, project)
+
+ project
+ end
+
+ def current_user(request)
+ request.env['warden']&.authenticate
+ end
end
end
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index dd99f9bb7d7..fee741b47be 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -26,7 +26,7 @@
module Gitlab
module Middleware
class Multipart
- RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'.freeze
class Handler
def initialize(env, message)
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
index 3fe32adeade..6105d165810 100644
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -8,16 +8,16 @@ module Gitlab
@proxy_host = opts.fetch(:proxy_host, 'localhost')
@proxy_port = opts.fetch(:proxy_port, 3808)
@proxy_path = opts[:proxy_path] if opts[:proxy_path]
- super(app, opts)
+
+ super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
end
def perform_request(env)
- unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
- return @app.call(env)
+ if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ super(env)
+ else
+ @app.call(env)
end
-
- env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
- super(env)
end
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 96ed20af918..fcf51b7fc5b 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -5,7 +5,7 @@
#
module Gitlab
module OAuth
- class SignupDisabledError < StandardError; end
+ SignupDisabledError = Class.new(StandardError)
class User
attr_accessor :auth_hash, :gl_user
@@ -29,12 +29,11 @@ module Gitlab
def save(provider = 'OAuth')
unauthorized_to_create unless gl_user
- if needs_blocking?
- gl_user.save!
- gl_user.block
- else
- gl_user.save!
- end
+ block_after_save = needs_blocking?
+
+ gl_user.save!
+
+ gl_user.block if block_after_save
log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 879d46446b3..962ff4d3985 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -1,12 +1,12 @@
module Gitlab
module OptimisticLocking
- extend self
+ module_function
def retry_lock(subject, retries = 100, &block)
loop do
begin
ActiveRecord::Base.transaction do
- return block.call(subject)
+ return yield(subject)
end
rescue ActiveRecord::StaleObjectError
retries -= 1
@@ -15,5 +15,7 @@ module Gitlab
end
end
end
+
+ alias_method :retry_optimistic_lock, :retry_lock
end
end
diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb
new file mode 100644
index 00000000000..62239779454
--- /dev/null
+++ b/lib/gitlab/prometheus.rb
@@ -0,0 +1,70 @@
+module Gitlab
+ PrometheusError = Class.new(StandardError)
+
+ # Helper methods to interact with Prometheus network services & resources
+ class Prometheus
+ attr_reader :api_url
+
+ def initialize(api_url:)
+ @api_url = api_url
+ end
+
+ def ping
+ json_api_get('query', query: '1')
+ end
+
+ def query(query)
+ get_result('vector') do
+ json_api_get('query', query: query)
+ end
+ end
+
+ def query_range(query, start: 8.hours.ago)
+ get_result('matrix') do
+ json_api_get('query_range',
+ query: query,
+ start: start.to_f,
+ end: Time.now.utc.to_f,
+ step: 1.minute.to_i)
+ end
+ end
+
+ private
+
+ def json_api_get(type, args = {})
+ get(join_api_url(type, args))
+ rescue Errno::ECONNREFUSED
+ raise PrometheusError, 'Connection refused'
+ end
+
+ def join_api_url(type, args = {})
+ url = URI.parse(api_url)
+ rescue URI::Error
+ raise PrometheusError, "Invalid API URL: #{api_url}"
+ else
+ url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
+ url.query = args.to_query
+
+ url.to_s
+ end
+
+ def get(url)
+ handle_response(HTTParty.get(url))
+ end
+
+ def handle_response(response)
+ if response.code == 200 && response['status'] == 'success'
+ response['data'] || {}
+ elsif response.code == 400
+ raise PrometheusError, response['error'] || 'Bad data received'
+ else
+ raise PrometheusError, "#{response.code} - #{response.body}"
+ end
+ end
+
+ def get_result(expected_type)
+ data = yield
+ data['result'] if data['resultType'] == expected_type
+ end
+ end
+end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9384102acec..bc5370de32a 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,27 +1,18 @@
# This file should not have any direct dependency on Rails environment
# please require all dependencies below:
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/module/delegation'
module Gitlab
class Redis
- CACHE_NAMESPACE = 'cache:gitlab'
- SESSION_NAMESPACE = 'session:gitlab'
- SIDEKIQ_NAMESPACE = 'resque:gitlab'
- MAILROOM_NAMESPACE = 'mail_room:gitlab'
- DEFAULT_REDIS_URL = 'redis://localhost:6379'
- CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
+ CACHE_NAMESPACE = 'cache:gitlab'.freeze
+ SESSION_NAMESPACE = 'session:gitlab'.freeze
+ SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
+ MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
+ DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
class << self
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- def params
- new.params
- end
-
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- # @deprecated Use .params instead to get sentinel support
- def url
- new.url
- end
+ delegate :params, :url, to: :new
def with
@pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@@ -42,13 +33,17 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
+ @_raw_config = ERB.new(File.read(config_file)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
@_raw_config
end
+
+ def config_file
+ ENV['GITLAB_REDIS_CONFIG_FILE'] || File.expand_path('../../config/resque.yml', __dir__)
+ end
end
def initialize(rails_env = nil)
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 437a339dd2b..7668ecacc4b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,7 +1,7 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user)
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index c77fe2d8bdc..5e5f5ff1589 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,17 +5,18 @@ module Gitlab
# The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
# Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
- # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+ # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
- NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
- NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
- PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
+ PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
# Same as NAMESPACE_REGEX_STR but allows `/` in the path.
# So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
- NAMESPACE_REF_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.\/]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
+ FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
new file mode 100644
index 00000000000..fef536ecb0b
--- /dev/null
+++ b/lib/gitlab/request_context.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ class RequestContext
+ class << self
+ def client_ip
+ RequestStore[:client_ip]
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ req = Rack::Request.new(env)
+
+ RequestStore[:client_ip] = req.ip
+
+ @app.call(env)
+ end
+ end
+end
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index 8130e55351e..0c9ab759e81 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -2,7 +2,7 @@ require 'fileutils'
module Gitlab
module RequestProfiler
- PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
+ PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles".freeze
def profile_token
Rails.cache.fetch('profile-token') do
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
index 72d00abfcc2..36791fae60f 100644
--- a/lib/gitlab/route_map.rb
+++ b/lib/gitlab/route_map.rb
@@ -1,6 +1,6 @@
module Gitlab
class RouteMap
- class FormatError < StandardError; end
+ FormatError = Class.new(StandardError)
def initialize(data)
begin
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index f253dc7477e..8a7cc690046 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,11 +28,12 @@ module Gitlab
if external_users_enabled? && @user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- @user.external = false
- else
- @user.external = true
- end
+ @user.external =
+ if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+ false
+ else
+ true
+ end
end
@user
diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb
index 7b6b70d8dbc..d50f826f924 100644
--- a/lib/gitlab/sanitizers/svg/whitelist.rb
+++ b/lib/gitlab/sanitizers/svg/whitelist.rb
@@ -6,18 +6,19 @@ module Gitlab
module SVG
class Whitelist
ALLOWED_ELEMENTS = %w[
- a altGlyph altGlyphDef altGlyphItem animate
- animateColor animateMotion animateTransform circle clipPath color-profile
- cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
- feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
- feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
- feImage feMerge feMergeNode feMorphology feOffset fePointLight
- feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
- font-face-format font-face-name font-face-src font-face-uri foreignObject
- g glyph glyphRef hkern image line linearGradient marker mask metadata
- missing-glyph mpath path pattern polygon polyline radialGradient rect
- script set stop style svg switch symbol text textPath title tref tspan use
- view vkern].freeze
+ a altGlyph altGlyphDef altGlyphItem animate
+ animateColor animateMotion animateTransform circle clipPath color-profile
+ cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
+ feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
+ feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
+ feImage feMerge feMergeNode feMorphology feOffset fePointLight
+ feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
+ font-face-format font-face-name font-face-src font-face-uri foreignObject
+ g glyph glyphRef hkern image line linearGradient marker mask metadata
+ missing-glyph mpath path pattern polygon polyline radialGradient rect
+ script set stop style svg switch symbol text textPath title tref tspan use
+ view vkern
+ ].freeze
ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index c9c65f76f4b..ccfa517e04b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -56,11 +56,12 @@ module Gitlab
def issues
issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
- if query =~ /#(\d+)\z/
- issues = issues.where(iid: $1)
- else
- issues = issues.full_search(query)
- end
+ issues =
+ if query =~ /#(\d+)\z/
+ issues.where(iid: $1)
+ else
+ issues.full_search(query)
+ end
issues.order('updated_at DESC')
end
@@ -73,11 +74,12 @@ module Gitlab
def merge_requests
merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
- if query =~ /[#!](\d+)\z/
- merge_requests = merge_requests.where(iid: $1)
- else
- merge_requests = merge_requests.full_search(query)
- end
+ merge_requests =
+ if query =~ /[#!](\d+)\z/
+ merge_requests.where(iid: $1)
+ else
+ merge_requests.full_search(query)
+ end
merge_requests.order('updated_at DESC')
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 7cf506ebe64..823f697f51c 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -1,24 +1,23 @@
+module DeliverNever
+ def deliver_later
+ self
+ end
+end
+
module Gitlab
class Seeder
def self.quiet
mute_mailer
SeedFu.quiet = true
+
yield
+
SeedFu.quiet = false
puts "\nOK".color(:green)
end
- def self.by_user(user)
- yield
- end
-
def self.mute_mailer
- code = <<-eos
-def Notify.deliver_later
- self
-end
- eos
- eval(code)
+ ActionMailer::MessageDelivery.prepend(DeliverNever)
end
end
end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index bf2c0acc729..9c92b83dddc 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -1,7 +1,7 @@
module Gitlab
module Serializer
class Pagination
- class InvalidResourceError < StandardError; end
+ InvalidResourceError = Class.new(StandardError)
include ::API::Helpers::Pagination
def initialize(request, response)
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 3faa336f142..da8d8ddb8ed 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -2,7 +2,7 @@ require 'securerandom'
module Gitlab
class Shell
- class Error < StandardError; end
+ Error = Class.new(StandardError)
KeyAdder = Struct.new(:io) do
def add_key(id, key)
@@ -82,8 +82,8 @@ module Gitlab
def import_repository(storage, name, url)
# Timeout should be less than 900 ideally, to prevent the memory killer
# to silently kill the process without knowing we are timing out here.
- output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, '800'])
+ output, status = Popen.popen([gitlab_shell_projects_path, 'import-project',
+ storage, "#{name}.git", url, '800'])
raise Error, output unless status.zero?
true
end
@@ -145,7 +145,7 @@ module Gitlab
# batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") }
def batch_add_keys(&block)
IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io|
- block.call(KeyAdder.new(io))
+ yield(KeyAdder.new(io))
end
end
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 4917c4ae2ac..99e56e923eb 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -94,11 +94,12 @@ module Gitlab
private
def raw_explain(query)
- if Gitlab::Database.postgresql?
- explain = "EXPLAIN ANALYZE #{query};"
- else
- explain = "EXPLAIN #{query};"
- end
+ explain =
+ if Gitlab::Database.postgresql?
+ "EXPLAIN ANALYZE #{query};"
+ else
+ "EXPLAIN #{query};"
+ end
ActiveRecord::Base.connection.execute(explain)
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index aadc401ff8d..11e5f1b645c 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -44,19 +44,42 @@ module Gitlab
# Returns true if all the given job have been completed.
#
- # jids - The Sidekiq job IDs to check.
+ # job_ids - The Sidekiq job IDs to check.
#
# Returns true or false.
- def self.all_completed?(jids)
- keys = jids.map { |jid| key_for(jid) }
+ def self.all_completed?(job_ids)
+ self.num_running(job_ids).zero?
+ end
+
+ # Returns the number of jobs that are running.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_running(job_ids)
+ responses = self.job_status(job_ids)
- responses = Sidekiq.redis do |redis|
+ responses.select(&:present?).count
+ end
+
+ # Returns the number of jobs that have completed.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ def self.num_completed(job_ids)
+ job_ids.size - self.num_running(job_ids)
+ end
+
+ # Returns the job status for each of the given job IDs.
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ #
+ # Returns an array of true or false indicating job completion.
+ def self.job_status(job_ids)
+ keys = job_ids.map { |jid| key_for(jid) }
+
+ Sidekiq.redis do |redis|
redis.pipelined do
keys.each { |key| redis.exists(key) }
end
end
-
- responses.all? { |value| !value }
end
def self.key_for(jid)
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index 22c39436cb2..cb7957e2af9 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -4,7 +4,7 @@ module Gitlab
module Finders
class RepoTemplateFinder < BaseTemplateFinder
# Raised when file is not found
- class FileNotFoundError < StandardError; end
+ FileNotFoundError = Class.new(StandardError)
def initialize(project, base_dir, extension, categories = {})
@categories = categories
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 9d2ecee9756..fd040148a1e 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def dropdown_names(context)
- categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+ categories = context == 'autodeploy' ? ['Auto deploy'] : %w(General Pages)
super().slice(*categories)
end
end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
index ce14cc887d0..8947ecfb92e 100644
--- a/lib/gitlab/update_path_error.rb
+++ b/lib/gitlab/update_path_error.rb
@@ -1,3 +1,3 @@
module Gitlab
- class UpdatePathError < StandardError; end
+ UpdatePathError = Class.new(StandardError)
end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index 4cc34e34460..961df0468a4 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -46,7 +46,7 @@ module Gitlab
git_tags = fetch_git_tags
git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ }
git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) }
- "v#{git_versions.sort.last.to_s}"
+ "v#{git_versions.sort.last}"
end
def fetch_git_tags
@@ -59,10 +59,10 @@ module Gitlab
"Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
"Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
"Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
- "Install gems" => %W(bundle),
- "Migrate DB" => %W(bundle exec rake db:migrate),
- "Recompile assets" => %W(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
- "Clear cache" => %W(bundle exec rake cache:clear)
+ "Install gems" => %w(bundle),
+ "Migrate DB" => %w(bundle exec rake db:migrate),
+ "Recompile assets" => %w(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
+ "Clear cache" => %w(bundle exec rake cache:clear)
}
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
new file mode 100644
index 00000000000..7e14a566696
--- /dev/null
+++ b/lib/gitlab/url_blocker.rb
@@ -0,0 +1,59 @@
+require 'resolv'
+
+module Gitlab
+ class UrlBlocker
+ class << self
+ # Used to specify what hosts and port numbers should be prohibited for project
+ # imports.
+ VALID_PORTS = [22, 80, 443].freeze
+
+ def blocked_url?(url)
+ return false if url.nil?
+
+ blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+ blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+ begin
+ uri = Addressable::URI.parse(url)
+ # Allow imports from the GitLab instance itself but only from the configured ports
+ return false if internal?(uri)
+
+ return true if blocked_port?(uri.port)
+
+ server_ips = Resolv.getaddresses(uri.hostname)
+ return true if (blocked_ips & server_ips).any?
+ rescue Addressable::URI::InvalidURIError
+ return true
+ end
+
+ false
+ end
+
+ private
+
+ def blocked_port?(port)
+ return false if port.blank?
+
+ port < 1024 && !VALID_PORTS.include?(port)
+ end
+
+ def internal?(uri)
+ internal_web?(uri) || internal_shell?(uri)
+ end
+
+ def internal_web?(uri)
+ uri.hostname == config.gitlab.host &&
+ (uri.port.blank? || uri.port == config.gitlab.port)
+ end
+
+ def internal_shell?(uri)
+ uri.hostname == config.gitlab_shell.ssh_host &&
+ (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
+ end
+
+ def config
+ Gitlab.config
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 19dad699edf..9ce13feb79a 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UrlSanitizer
def self.sanitize(content)
- regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
+ regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
@@ -9,6 +9,8 @@ module Gitlab
end
def self.valid?(url)
+ return false unless url
+
Addressable::URI.parse(url.strip)
true
@@ -16,6 +18,12 @@ module Gitlab
false
end
+ def self.http_credentials_for_user(user)
+ return {} unless user.respond_to?(:username)
+
+ { user: user.username }
+ end
+
def initialize(url, credentials: nil)
@url = Addressable::URI.parse(url.strip)
@credentials = credentials
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 6ce9b229294..f260c0c535f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def can_do_action?(action)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
@@ -19,7 +19,7 @@ module Gitlab
end
def allowed?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -29,7 +29,7 @@ module Gitlab
end
def can_push_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
@@ -44,7 +44,7 @@ module Gitlab
end
def can_merge_to_branch?(ref)
- return false if no_user_or_blocked?
+ return false unless can_access_git?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -55,15 +55,15 @@ module Gitlab
end
def can_read_project?
- return false if no_user_or_blocked?
+ return false unless can_access_git?
user.can?(:read_project, project)
end
private
- def no_user_or_blocked?
- user.nil? || user.blocked?
+ def can_access_git?
+ user && user.can?(:access_git)
end
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index a4e966e4016..8f1d1fdc02e 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -33,8 +33,10 @@ module Gitlab
PUBLIC = 20 unless const_defined?(:PUBLIC)
class << self
- def values
- options.values
+ delegate :values, to: :options
+
+ def string_values
+ string_options.keys
end
def options
@@ -45,6 +47,14 @@ module Gitlab
}
end
+ def string_options
+ {
+ 'private' => PRIVATE,
+ 'internal' => INTERNAL,
+ 'public' => PUBLIC
+ }
+ end
+
def highest_allowed_level
restricted_levels = current_application_settings.restricted_visibility_levels
@@ -84,18 +94,39 @@ module Gitlab
level_name
end
+
+ def level_value(level)
+ return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+ string_options[level] || PRIVATE
+ end
+
+ def string_level(level)
+ string_options.key(level)
+ end
end
def private?
- visibility_level_field == PRIVATE
+ visibility_level_value == PRIVATE
end
def internal?
- visibility_level_field == INTERNAL
+ visibility_level_value == INTERNAL
end
def public?
- visibility_level_field == PUBLIC
+ visibility_level_value == PUBLIC
+ end
+
+ def visibility_level_value
+ self[visibility_level_field]
+ end
+
+ def visibility
+ Gitlab::VisibilityLevel.string_level(visibility_level_value)
+ end
+
+ def visibility=(level)
+ self[visibility_level_field] = Gitlab::VisibilityLevel.level_value(level)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c8872df8a93..eae1a0abf06 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -4,10 +4,11 @@ require 'securerandom'
module Gitlab
class Workhorse
- SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
- VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
- INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
- INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+ SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'.freeze
+ VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
+ NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
@@ -154,6 +155,18 @@ module Gitlab
Rails.root.join('.gitlab_workhorse_secret')
end
+ def set_key_and_notify(key, value, expire: nil, overwrite: true)
+ Gitlab::Redis.with do |redis|
+ result = redis.set(key, value, ex: expire, nx: !overwrite)
+ if result
+ redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}")
+ value
+ else
+ redis.get(key)
+ end
+ end
+ end
+
protected
def encode(hash)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index e55c0d6ac49..3d60618006c 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -1,5 +1,5 @@
module Mattermost
- class ClientError < Mattermost::Error; end
+ ClientError = Class.new(Mattermost::Error)
class Client
attr_reader :user
@@ -26,7 +26,7 @@ module Mattermost
def session_get(path, options = {})
with_session do |session|
- get(session, path, options)
+ get(session, path, options)
end
end
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
index 014df175be0..dee6deb7974 100644
--- a/lib/mattermost/error.rb
+++ b/lib/mattermost/error.rb
@@ -1,3 +1,3 @@
module Mattermost
- class Error < StandardError; end
+ Error = Class.new(StandardError)
end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 377cb7b1021..688a79c0441 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -5,7 +5,7 @@ module Mattermost
end
end
- class ConnectionError < Mattermost::Error; end
+ ConnectionError = Class.new(Mattermost::Error)
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
@@ -153,7 +153,7 @@ module Mattermost
yield
rescue HTTParty::Error => e
raise Mattermost::ConnectionError.new(e.message)
- rescue Errno::ECONNREFUSED
+ rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError.new(e.message)
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 09dfd082b3a..2cdbbdece16 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,18 @@
module Mattermost
class Team < Client
+ # Returns **all** teams for an admin
def all
- session_get('/api/v3/teams/all')
+ session_get('/api/v3/teams/all').values
+ end
+
+ # Creates a team on the linked Mattermost instance, the team admin will be the
+ # `current_user` passed to the Mattermost::Client instance
+ def create(name:, display_name:, type:)
+ session_post('/api/v3/teams/create', body: {
+ name: name,
+ display_name: display_name,
+ type: type
+ }.to_json)
end
end
end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
index 5a7d67c2390..5a7d67c2390 100644
--- a/lib/omniauth/strategies/bitbucket.rb
+++ b/lib/omni_auth/strategies/bitbucket.rb
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 4edfd015074..be0d97370d0 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -5,10 +5,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
- # [+linenostart+] The line number for the first line (default: 1).
- def initialize(linenostart: 1)
- @linenostart = linenostart
- @line_number = linenostart
+ # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
+ def initialize(tag: nil)
+ @line_number = 1
+ @tag = tag
end
def stream(tokens, &b)
@@ -17,7 +17,7 @@ module Rouge
yield "\n" unless is_first
is_first = false
- yield %(<span id="LC#{@line_number}" class="line">)
+ yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
line.each { |token, value| yield span(token, value.chomp) }
yield %(</span>)
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index e5797d8fe3c..f6642527639 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -56,14 +56,14 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
# as well. For example:
#
-# -listen-http 1.1.1.1:80
+# -listen-http 1.1.1.1:80 -listen-http [2001::1]:80
#
# To enable HTTPS support for custom domains add the `-listen-https`,
# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
# as well. For example:
#
-# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+# -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
#
# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31..f25e66d54c8 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
## See app/controllers/application_controller.rb for headers set
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5661394058d..2b40da18bab 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -82,6 +82,16 @@ server {
##
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
+ ## [Optional] Enable HTTP Strict Transport Security
+ # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index d5a402907d8..2301ec9b228 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%W(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
+ if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 78ae187817a..d55923673b1 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,7 +1,7 @@
namespace :cache do
namespace :clear do
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
- REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+ REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
desc "GitLab | Clear redis cache"
task redis: :environment do
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 5e94fba97bf..e65609d7001 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -2,7 +2,7 @@ task dev: ["dev:setup"]
namespace :dev do
desc "GitLab | Setup developer environment (db, fixtures)"
- task :setup => :environment do
+ task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
index afe5d42910c..557f4fef10b 100644
--- a/lib/tasks/downtime_check.rake
+++ b/lib/tasks/downtime_check.rake
@@ -1,10 +1,10 @@
desc 'Checks if migrations in a branch require downtime'
task downtime_check: :environment do
- if defined?(Gitlab::License)
- repo = 'gitlab-ee'
- else
- repo = 'gitlab-ce'
- end
+ repo = if defined?(Gitlab::License)
+ 'gitlab-ee'
+ else
+ 'gitlab-ce'
+ end
`git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index e9587595fef..7ad2b2e4d39 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
task :flay do
- output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+ output = `bundle exec flay --mass 35 app/ lib/gitlab/`
if output.include? "Similar code found"
puts output
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 993112aee3b..5293f5af12d 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,33 +1,36 @@
namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
- task digests: :environment do
+ task digests: ['yarn:check', 'environment'] do
require 'digest/sha2'
require 'json'
- dir = Gemojione.images_path
- digests = []
- aliases = Hash.new { |hash, key| hash[key] = [] }
- aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
-
- JSON.parse(File.read(aliases_path)).each do |alias_name, real_name|
- aliases[real_name] << alias_name
- end
-
- Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
- fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
- digest = Digest::SHA256.file(fpath).hexdigest
+ # We don't have `node_modules` available in built versions of GitLab
+ FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis'))
- digests << { name: name, unicode: emoji_hash['unicode'], digest: digest }
+ dir = Gemojione.images_path
+ resultant_emoji_map = {}
+
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+ hash_digest = Digest::SHA256.file(fpath).hexdigest
+
+ entry = {
+ category: emoji_hash['category'],
+ moji: emoji_hash['moji'],
+ unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
+ digest: hash_digest,
+ }
- aliases[name].each do |alias_name|
- digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
+ resultant_emoji_map[name] = entry
end
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
- handle.write(JSON.pretty_generate(digests))
+ handle.write(JSON.pretty_generate(resultant_emoji_map))
end
end
@@ -55,21 +58,40 @@ namespace :gemojione do
SPRITESHEET_WIDTH = 860
SPRITESHEET_HEIGHT = 840
+ # Setup a map to rename image files
+ emoji_unicode_string_to_name_map = {}
+ Gitlab::Emoji.emojis.each do |name, emoji_hash|
+ # Ignore aliases
+ unless Gitlab::Emoji.emojis_aliases.key?(name)
+ emoji_unicode_string_to_name_map[emoji_hash['unicode']] = name
+ end
+ end
+
+ # Copy the Gemojione assets to the temporary folder for renaming
+ emoji_dir = "app/assets/images/emoji"
+ FileUtils.rm_rf(emoji_dir)
+ FileUtils.mkdir_p(emoji_dir, mode: 0700)
+ FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
+ Dir[File.join(emoji_dir, "**/*.png")].each do |png|
+ image_path = png
+ rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ end
+
Dir.mktmpdir do |tmpdir|
- # Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), SIZE)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, SIZE)
end
end
- style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
+ style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
- SpriteFactory.run!(File.join(tmpdir, 'png'), {
+ SpriteFactory.run!(tmpdir, {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
@@ -83,7 +105,7 @@ namespace :gemojione do
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
- system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
+ system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
@@ -92,6 +114,8 @@ namespace :gemojione do
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
height: #{SIZE}px;
width: #{SIZE}px;
@@ -112,16 +136,17 @@ namespace :gemojione do
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.images_path, tmpdir)
+ FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
- resize!(File.join(tmpdir, png), RETINA)
+ tmp_image_path = File.join(tmpdir, png)
+ resize!(tmp_image_path, RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
- SpriteFactory.run!(File.join(tmpdir), {
+ SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
@@ -155,4 +180,20 @@ namespace :gemojione do
image.write(image_path) { self.quality = 100 }
image.destroy!
end
+
+ EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
+ def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
+ # Rename file from unicode to emoji name
+ matches = EMOJI_IMAGE_PATH_RE.match(image_path)
+ preceding_path = matches[1]
+ unicode_string = matches[2]
+ name = emoji_unicode_string_to_name_map[unicode_string]
+ if name
+ new_png_path = File.join(preceding_path, "#{name}.png")
+ FileUtils.mv(image_path, new_png_path)
+ new_png_path
+ else
+ puts "Warning: emoji_unicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
+ end
+ end
end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 3eb5fc07b3c..098f9851b45 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -20,7 +20,7 @@ namespace :gitlab do
desc 'GitLab | Assets | Fix all absolute url references in CSS'
task :fix_urls do
css_files = Dir['public/assets/*.css']
- css_files.each do | file |
+ css_files.each do |file|
# replace url(/assets/*) with url(./*)
puts "Fixing #{file}"
system "sed", "-i", "-e", 's/url(\([\"\']\?\)\/assets\//url(\1.\//g', file
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 6102517e730..a6f8c4ced5d 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -6,8 +6,6 @@ namespace :gitlab do
gitlab:ldap:check
gitlab:app:check}
-
-
namespace :app do
desc "GitLab | Check the configuration of the GitLab Rails app"
task check: :environment do
@@ -34,7 +32,6 @@ namespace :gitlab do
finished_checking "GitLab"
end
-
# Checks
########################
@@ -194,7 +191,7 @@ namespace :gitlab do
def check_migrations_are_up
print "All migrations up? ... "
- migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
+ migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
unless migration_status =~ /down\s+\d{14}/
puts "yes".color(:green)
@@ -279,7 +276,7 @@ namespace :gitlab do
upload_path_tmp = File.join(upload_path, 'tmp')
if File.stat(upload_path).mode == 040700
- unless Dir.exists?(upload_path_tmp)
+ unless Dir.exist?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
return
end
@@ -316,7 +313,7 @@ namespace :gitlab do
min_redis_version = "2.8.0"
print "Redis version >= #{min_redis_version}? ... "
- redis_version = run_command(%W(redis-cli --version))
+ redis_version = run_command(%w(redis-cli --version))
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -351,14 +348,14 @@ namespace :gitlab do
finished_checking "GitLab Shell"
end
-
# Checks
########################
def check_repo_base_exists
puts "Repo base directory exists?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
if File.exist?(repo_base_path)
@@ -382,12 +379,13 @@ namespace :gitlab do
def check_repo_base_is_not_symlink
puts "Repo storage directories are symlinks?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
unless File.symlink?(repo_base_path)
@@ -405,12 +403,13 @@ namespace :gitlab do
def check_repo_base_permissions
puts "Repo paths access is drwxrws---?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
@@ -435,12 +434,13 @@ namespace :gitlab do
gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
uid = uid_for(gitlab_shell_ssh_user)
@@ -493,7 +493,6 @@ namespace :gitlab do
)
fix_and_rerun
end
-
end
end
@@ -565,8 +564,6 @@ namespace :gitlab do
end
end
-
-
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
@@ -579,7 +576,6 @@ namespace :gitlab do
finished_checking "Sidekiq"
end
-
# Checks
########################
@@ -621,12 +617,11 @@ namespace :gitlab do
end
def sidekiq_process_count
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
end
end
-
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
@@ -649,7 +644,6 @@ namespace :gitlab do
finished_checking "Reply by email"
end
-
# Checks
########################
@@ -757,7 +751,7 @@ namespace :gitlab do
end
def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
ps_ux.include?("mail_room")
end
end
@@ -805,13 +799,13 @@ namespace :gitlab do
def check_ldap_auth(adapter)
auth = adapter.config.has_auth?
- if auth && adapter.ldap.bind
- message = 'Success'.color(:green)
- elsif auth
- message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
- else
- message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
- end
+ message = if auth && adapter.ldap.bind
+ 'Success'.color(:green)
+ elsif auth
+ 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
puts "LDAP authentication... #{message}"
end
@@ -820,8 +814,8 @@ namespace :gitlab do
namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
- Gitlab.config.repositories.storages.each do |name, path|
- namespace_dirs = Dir.glob(File.join(path, '*'))
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*'))
namespace_dirs.each do |namespace_dir|
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
@@ -838,11 +832,11 @@ namespace :gitlab do
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
- File.join(
- p.repository_storage_path,
- "#{p.path_with_namespace}.git"
- )
- end
+ File.join(
+ p.repository_storage_path,
+ "#{p.path_with_namespace}.git"
+ )
+ end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
else
@@ -855,7 +849,7 @@ namespace :gitlab do
##########################
def fix_and_rerun
- puts " Please #{"fix the error above"} and rerun the checks.".color(:red)
+ puts " Please fix the error above and rerun the checks.".color(:red)
end
def for_more_information(*sources)
@@ -917,7 +911,7 @@ namespace :gitlab do
def check_ruby_version
required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
+ current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
print "Ruby version >= #{required_version} ? ... "
@@ -988,13 +982,13 @@ namespace :gitlab do
end
def check_config_lock(repo_dir)
- config_exists = File.exist?(File.join(repo_dir,'config.lock'))
+ config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
- lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
+ lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
if lock_files.present?
puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file|
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 967f630ef20..f76bef5f4bf 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -6,7 +6,8 @@ namespace :gitlab do
remove_flag = ENV['REMOVE']
namespaces = Namespace.pluck(:path)
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ git_base_path = repository_storage['path']
all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.color(:yellow)
@@ -25,7 +26,6 @@ namespace :gitlab do
end
all_dirs.each do |dir_path|
-
if remove_flag
if FileUtils.rm_rf dir_path
puts "Removed...#{dir_path}".color(:red)
@@ -48,16 +48,17 @@ namespace :gitlab do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
- Gitlab.config.repositories.storages.each do |name, repo_root|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_root = repository_storage['path']
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
path.chomp!
- repo_with_namespace = path.
- sub(repo_root, '').
- sub(%r{^/*}, '').
- chomp('.git').
- chomp('.wiki')
+ repo_with_namespace = path
+ .sub(repo_root, '')
+ .sub(%r{^/*}, '')
+ .chomp('.git')
+ .chomp('.wiki')
next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 7c96bc864ce..5476438b8fa 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
end
desc 'Drop all tables'
- task :drop_tables => :environment do
+ task drop_tables: :environment do
connection = ActiveRecord::Base.connection
# If MySQL, turn off foreign key checks
@@ -62,9 +62,9 @@ namespace :gitlab do
ref = Shellwords.escape(args[:ref])
- migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines.
- map { |file| Rails.root.join(file.strip).to_s }.
- select { |file| File.file?(file) }
+ migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines
+ .map { |file| Rails.root.join(file.strip).to_s }
+ .select { |file| File.file?(file) }
Gitlab::DowntimeCheck.new.check_and_print(migrations)
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7db0779def8..7ccda04a35f 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,7 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_BUILD_REF_NAME'] }
+ { branch: ENV['CI_COMMIT_REF_NAME'] }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index a67c1fe1f27..cf82134d97e 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -1,6 +1,5 @@
namespace :gitlab do
namespace :git do
-
desc "GitLab | Git | Repack"
task repack: :environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
@@ -50,6 +49,5 @@ namespace :gitlab do
puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
-
end
end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index b4015f5238e..48bd9139ce8 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -11,7 +11,8 @@ namespace :gitlab do
#
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
task repos: :environment do
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ git_base_path = repository_storage['path']
repos_to_import = Dir.glob(git_base_path + '/**/*.git')
repos_to_import.each do |repo_path|
@@ -46,7 +47,7 @@ namespace :gitlab do
group = Namespace.find_by(path: group_name)
# create group namespace
unless group
- group = Group.new(:name => group_name)
+ group = Group.new(name: group_name)
group.path = group_name
group.owner = user
if group.save
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index c2c6031db67..dd1825c8a9e 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
desc "GitLab | Display exported DB structure"
task data: :environment do
- puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index f7c831892ee..a2a2db487b7 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -2,24 +2,25 @@ namespace :gitlab do
namespace :env do
desc "GitLab | Show information about GitLab and its environment"
task info: :environment do
-
# check if there is an RVM environment
- rvm_version = run_and_match(%W(rvm --version), /[\d\.]+/).try(:to_s)
+ rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version
- ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
+ ruby_version = run_and_match(%w(ruby --version), /[\d\.p]+/).try(:to_s)
# check Gem version
- gem_version = run_command(%W(gem --version))
+ gem_version = run_command(%w(gem --version))
# check Bundler version
- bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
+ bunder_version = run_and_match(%w(bundle --version), /[\d\.]+/).try(:to_s)
# check Rake version
- rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s)
# check redis version
- redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+ redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+ # check Git version
+ git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a
puts ""
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
- puts "Current User:\t#{run_command(%W(whoami))}"
+ puts "Current User:\t#{run_command(%w(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
@@ -27,9 +28,9 @@ namespace :gitlab do
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
+ puts "Git Version:\t#{git_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
-
# check database adapter
database_adapter = ActiveRecord::Base.connection.adapter_name.downcase
@@ -54,8 +55,6 @@ namespace :gitlab do
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
-
-
# check Gitolite version
gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION"
if File.readable?(gitlab_shell_version_file)
@@ -66,12 +65,11 @@ namespace :gitlab do
puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repository storage paths:"
- Gitlab.config.repositories.storages.each do |name, path|
- puts "- #{name}: \t#{path}"
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ puts "- #{name}: \t#{repository_storage['path']}"
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
-
end
end
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 5a09cd7ce41..dd2fda54e62 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -20,10 +20,10 @@ namespace :gitlab do
config = {
user: Gitlab.config.gitlab.user,
gitlab_url: gitlab_url,
- http_settings: {self_signed_cert: false}.stringify_keys,
+ http_settings: { self_signed_cert: false }.stringify_keys,
auth_file: File.join(user_home, ".ssh", "authorized_keys"),
redis: {
- bin: %x{which redis-cli}.chomp,
+ bin: `which redis-cli`.chomp,
namespace: "resque:gitlab"
}.stringify_keys,
log_level: "INFO",
@@ -43,7 +43,7 @@ namespace :gitlab do
File.open("config.yml", "w+") {|f| f.puts config.to_yaml}
# Launch installation process
- system(*%W(bin/install) + repository_storage_paths_args)
+ system(*%w(bin/install) + repository_storage_paths_args)
end
# (Re)create hooks
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index f2e12d85045..6cbc83b8973 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -1,9 +1,9 @@
namespace :gitlab do
namespace :sidekiq do
- QUEUE = 'queue:post_receive'
+ QUEUE = 'queue:post_receive'.freeze
desc 'Drop all Sidekiq PostReceive jobs for a given project'
- task :drop_post_receive , [:project] => :environment do |t, args|
+ task :drop_post_receive, [:project] => :environment do |t, args|
unless args.project.present?
abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
end
@@ -21,7 +21,7 @@ namespace :gitlab do
# new jobs already. We will repopulate it with the old jobs, skipping the
# ones we want to drop.
dropped = 0
- while (job = redis.lpop(temp_queue)) do
+ while (job = redis.lpop(temp_queue))
if repo_path(job) == project_path
dropped += 1
else
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e128738b5f8..bb755ae689b 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -19,23 +19,20 @@ module Gitlab
# It will primarily use lsb_relase to determine the OS.
# It has fallbacks to Debian, SuSE, OS X and systems running systemd.
def os_name
- os_name = run_command(%W(lsb_release -irs))
- os_name ||= if File.readable?('/etc/system-release')
- File.read('/etc/system-release')
- end
- os_name ||= if File.readable?('/etc/debian_version')
- debian_version = File.read('/etc/debian_version')
- "Debian #{debian_version}"
- end
- os_name ||= if File.readable?('/etc/SuSE-release')
- File.read('/etc/SuSE-release')
- end
- os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
- "Mac OS X #{os_x_version}"
- end
- os_name ||= if File.readable?('/etc/os-release')
- File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
- end
+ os_name = run_command(%w(lsb_release -irs))
+ os_name ||=
+ if File.readable?('/etc/system-release')
+ File.read('/etc/system-release')
+ elsif File.readable?('/etc/debian_version')
+ "Debian #{File.read('/etc/debian_version')}"
+ elsif File.readable?('/etc/SuSE-release')
+ File.read('/etc/SuSE-release')
+ elsif os_x_version = run_command(%w(sw_vers -productVersion))
+ "Mac OS X #{os_x_version}"
+ elsif File.readable?('/etc/os-release')
+ File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
+ end
+
os_name.try(:squish!)
end
@@ -104,7 +101,7 @@ module Gitlab
def warn_user_is_not_gitlab
unless @warned_user_not_gitlab
gitlab_user = Gitlab.config.gitlab.user
- current_user = run_command(%W(whoami)).chomp
+ current_user = run_command(%w(whoami)).chomp
unless current_user == gitlab_user
puts " Warning ".color(:black).background(:yellow)
puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
@@ -133,8 +130,8 @@ module Gitlab
end
def all_repos
- Gitlab.config.repositories.storages.each do |name, path|
- IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
find.each_line do |path|
yield path.chomp
end
@@ -143,7 +140,7 @@ module Gitlab
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
def user_home
@@ -171,14 +168,14 @@ module Gitlab
def reset_to_tag(tag_wanted, target_dir)
tag =
- begin
- # First try to checkout without fetching
- # to avoid stalling tests if the Internet is down.
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
- rescue Gitlab::TaskFailedError
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
- end
+ begin
+ # First try to checkout without fetching
+ # to avoid stalling tests if the Internet is down.
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
+ rescue Gitlab::TaskFailedError
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
+ end
if tag
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 84810b489ce..523b0fa055b 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -2,15 +2,15 @@ namespace :gitlab do
desc "GitLab | Run all tests"
task :test do
cmds = [
- %W(rake brakeman),
- %W(rake rubocop),
- %W(rake spinach),
- %W(rake spec),
- %W(rake karma)
+ %w(rake brakeman),
+ %w(rake rubocop),
+ %w(rake spinach),
+ %w(rake spec),
+ %w(rake karma)
]
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
end
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
index 84aa2e8507a..6f101aea303 100644
--- a/lib/tasks/gitlab/track_deployment.rake
+++ b/lib/tasks/gitlab/track_deployment.rake
@@ -1,8 +1,8 @@
namespace :gitlab do
desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
task track_deployment: :environment do
- metric = Gitlab::Metrics::Metric.
- new('deployments', version: Gitlab::VERSION)
+ metric = Gitlab::Metrics::Metric
+ .new('deployments', version: Gitlab::VERSION)
Gitlab::Metrics.submit_metrics([metric.to_hash])
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index b77a5bb62d1..dbdfb335a5c 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
- ]
+ ].freeze
def vendor_directory
Rails.root.join('vendor')
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 49530e7a372..5a1c8006052 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :web_hook do
desc "GitLab | Adds a webhook to the projects"
- task :add => :environment do
+ task add: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -21,7 +21,7 @@ namespace :gitlab do
end
desc "GitLab | Remove a webhook from the projects"
- task :rm => :environment do
+ task rm: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -34,7 +34,7 @@ namespace :gitlab do
end
desc "GitLab | List webhooks"
- task :list => :environment do
+ task list: :environment do
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 32b668df3bf..7b63e93db0e 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -6,4 +6,3 @@ unless Rails.env.production?
end
end
end
-
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index 4f2486157b7..fc2cea8c016 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -24,7 +24,7 @@ task migrate_iids: :environment do
else
print 'F'
end
- rescue => ex
+ rescue
print 'F'
end
end
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 39541c0b9c6..56b81106c5f 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -76,23 +76,23 @@ namespace :services do
end
param_hash
- end.sort_by { |p| p[:required] ? 0 : 1 }
+ end
+ service_hash[:params].sort_by! { |p| p[:required] ? 0 : 1 }
- puts "Collected data for: #{service.title}, #{Time.now-service_start}"
+ puts "Collected data for: #{service.title}, #{Time.now - service_start}"
service_hash
end
doc_start = Time.now
doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
- result = ERB.new(services_template, 0 , '>')
+ result = ERB.new(services_template, 0, '>')
.result(OpenStruct.new(services: services).instance_eval { binding })
File.open(doc_path, 'w') do |f|
f.write result
end
- puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}"
-
+ puts "write a new service.md to: #{doc_path}, #{Time.now - doc_start}"
end
end
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
index d1f6ed87704..dd9ce86f7ca 100644
--- a/lib/tasks/sidekiq.rake
+++ b/lib/tasks/sidekiq.rake
@@ -1,21 +1,21 @@
namespace :sidekiq do
desc "GitLab | Stop sidekiq"
task :stop do
- system *%W(bin/background_jobs stop)
+ system(*%w(bin/background_jobs stop))
end
desc "GitLab | Start sidekiq"
task :start do
- system *%W(bin/background_jobs start)
+ system(*%w(bin/background_jobs start))
end
desc 'GitLab | Restart sidekiq'
task :restart do
- system *%W(bin/background_jobs restart)
+ system(*%w(bin/background_jobs restart))
end
desc "GitLab | Start sidekiq with launchd on Mac OS X"
task :launchd do
- system *%W(bin/background_jobs start_no_deamonize)
+ system(*%w(bin/background_jobs start_no_deamonize))
end
end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 2cf7a25a0fd..602c60be828 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -4,8 +4,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run request specs'
task :api do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @api)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @api)
]
run_commands(cmds)
end
@@ -13,8 +13,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run feature specs'
task :feature do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @feature)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @feature)
]
run_commands(cmds)
end
@@ -22,8 +22,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run model specs'
task :models do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @models)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @models)
]
run_commands(cmds)
end
@@ -31,8 +31,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run service specs'
task :services do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @services)
]
run_commands(cmds)
end
@@ -40,8 +40,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run lib specs'
task :lib do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @lib)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @lib)
]
run_commands(cmds)
end
@@ -49,8 +49,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
]
run_commands(cmds)
end
@@ -59,14 +59,14 @@ end
desc "GitLab | Run specs"
task :spec do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec),
+ %w(rake gitlab:setup),
+ %w(rspec spec),
]
run_commands(cmds)
end
def run_commands(cmds)
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 8dbfa7751dc..19ff13f06c0 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -35,7 +35,7 @@ task :spinach do
end
def run_system_command(cmd)
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
end
def run_spinach_command(args)
diff --git a/package.json b/package.json
index ad0aaef1897..b3d038bd3d1 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,11 @@
"private": true,
"scripts": {
"dev-server": "webpack-dev-server --config config/webpack.config.js",
- "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
- "eslint-fix": "eslint --max-warnings 0 --ext .js,.js.es6 --fix .",
- "eslint-report": "eslint --max-warnings 0 --ext .js,.js.es6 --format html --output-file ./eslint-report.html .",
+ "eslint": "eslint --max-warnings 0 --ext .js .",
+ "eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
+ "eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
"karma": "karma start config/karma.config.js --single-run",
+ "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
"karma-start": "karma start config/karma.config.js",
"webpack": "webpack --config config/webpack.config.js",
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
@@ -13,26 +14,31 @@
"dependencies": {
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
- "babel-preset-es2015": "^6.22.0",
+ "babel-plugin-transform-define": "^1.2.0",
+ "babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
+ "core-js": "^2.4.1",
"d3": "^3.5.11",
+ "document-register-element": "^1.3.0",
"dropzone": "^4.2.0",
- "es6-promise": "^4.0.5",
+ "emoji-unicode-version": "^0.2.1",
"jquery": "^2.2.1",
- "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4",
"jquery-ujs": "^1.2.1",
"js-cookie": "^2.1.3",
"mousetrap": "^1.4.6",
"pikaday": "^1.5.1",
+ "raphael": "^2.2.7",
+ "raw-loader": "^0.5.1",
"select2": "3.5.2-browserify",
"stats-webpack-plugin": "^0.4.3",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
- "vue": "^2.0.3",
+ "vue": "^2.1.10",
"vue-resource": "^0.9.3",
- "webpack": "^2.2.1"
+ "webpack": "^2.2.1",
+ "webpack-bundle-analyzer": "^2.3.0"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
@@ -53,13 +59,5 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.2",
"webpack-dev-server": "^2.3.0"
- },
- "nyc": {
- "exclude": [
- "spec/javascripts/test_bundle.js",
- "spec/javascripts/**/*_spec.js",
- "spec/javascripts/**/*_spec.js.es6",
- "app/assets/javascripts/droplab/**/*"
- ]
}
}
diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg
deleted file mode 100644
index 922e28bf696..00000000000
--- a/public/ci/build-canceled.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg
deleted file mode 100644
index 1aefd3f1761..00000000000
--- a/public/ci/build-failed.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg
deleted file mode 100644
index 536931af84d..00000000000
--- a/public/ci/build-pending.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg
deleted file mode 100644
index 0d71eef3c34..00000000000
--- a/public/ci/build-running.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg
deleted file mode 100644
index f15507188e0..00000000000
--- a/public/ci/build-skipped.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">skipped</text><text x="66" y="14">skipped</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg
deleted file mode 100644
index 43b67e45f42..00000000000
--- a/public/ci/build-success.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg
deleted file mode 100644
index c72a2f5a7f5..00000000000
--- a/public/ci/build-unknown.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg> \ No newline at end of file
diff --git a/qa/.gitignore b/qa/.gitignore
new file mode 100644
index 00000000000..3fec32c8427
--- /dev/null
+++ b/qa/.gitignore
@@ -0,0 +1 @@
+tmp/
diff --git a/qa/.rspec b/qa/.rspec
new file mode 100644
index 00000000000..b83d9b7aa65
--- /dev/null
+++ b/qa/.rspec
@@ -0,0 +1,3 @@
+--color
+--format documentation
+--require spec_helper
diff --git a/qa/Dockerfile b/qa/Dockerfile
new file mode 100644
index 00000000000..72c82503542
--- /dev/null
+++ b/qa/Dockerfile
@@ -0,0 +1,17 @@
+FROM ruby:2.3
+LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
+
+RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
+ apt-get update && apt-get install -y --force-yes \
+ libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
+ apt-get clean
+
+WORKDIR /home/qa
+
+COPY ./Gemfile* ./
+
+RUN bundle install
+
+COPY ./ ./
+
+ENTRYPOINT ["bin/test"]
diff --git a/qa/Gemfile b/qa/Gemfile
new file mode 100644
index 00000000000..6bfe25ba437
--- /dev/null
+++ b/qa/Gemfile
@@ -0,0 +1,7 @@
+source 'https://rubygems.org'
+
+gem 'capybara', '~> 2.12.1'
+gem 'capybara-screenshot', '~> 1.0.14'
+gem 'capybara-webkit', '~> 1.12.0'
+gem 'rake', '~> 12.0.0'
+gem 'rspec', '~> 3.5'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
new file mode 100644
index 00000000000..6de2abff198
--- /dev/null
+++ b/qa/Gemfile.lock
@@ -0,0 +1,61 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.5.0)
+ public_suffix (~> 2.0, >= 2.0.2)
+ capybara (2.12.1)
+ addressable
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ capybara-screenshot (1.0.14)
+ capybara (>= 1.0, < 3)
+ launchy
+ capybara-webkit (1.12.0)
+ capybara (>= 2.3.0, < 2.13.0)
+ json
+ diff-lcs (1.3)
+ json (2.0.3)
+ launchy (2.4.3)
+ addressable (~> 2.3)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
+ mini_portile2 (2.1.0)
+ nokogiri (1.7.0.1)
+ mini_portile2 (~> 2.1.0)
+ public_suffix (2.0.5)
+ rack (2.0.1)
+ rack-test (0.6.3)
+ rack (>= 1.0)
+ rake (12.0.0)
+ rspec (3.5.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-core (3.5.4)
+ rspec-support (~> 3.5.0)
+ rspec-expectations (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-mocks (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-support (3.5.0)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ capybara (~> 2.12.1)
+ capybara-screenshot (~> 1.0.14)
+ capybara-webkit (~> 1.12.0)
+ rake (~> 12.0.0)
+ rspec (~> 3.5)
+
+BUNDLED WITH
+ 1.14.6
diff --git a/qa/README.md b/qa/README.md
new file mode 100644
index 00000000000..b6b5a76f1d3
--- /dev/null
+++ b/qa/README.md
@@ -0,0 +1,18 @@
+## Integration tests for GitLab
+
+This directory contains integration tests for GitLab.
+
+It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+
+## What GitLab QA is?
+
+GitLab QA is an integration tests suite for GitLab.
+
+These are black-box and entirely click-driven integration tests you can run
+against any existing instance.
+
+## How does it work?
+
+1. When we release a new version of GitLab, we build a Docker images for it.
+1. Along with GitLab Docker Images we also build and publish GitLab QA images.
+1. GitLab QA project uses these images to execute integration tests.
diff --git a/qa/bin/qa b/qa/bin/qa
new file mode 100755
index 00000000000..cecdeac14db
--- /dev/null
+++ b/qa/bin/qa
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require_relative '../qa'
+
+QA::Scenario
+ .const_get(ARGV.shift)
+ .perform(*ARGV)
diff --git a/qa/bin/test b/qa/bin/test
new file mode 100755
index 00000000000..997392ad6e4
--- /dev/null
+++ b/qa/bin/test
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+xvfb-run bundle exec bin/qa $@
diff --git a/qa/qa.rb b/qa/qa.rb
new file mode 100644
index 00000000000..58cf615cc9f
--- /dev/null
+++ b/qa/qa.rb
@@ -0,0 +1,81 @@
+$: << File.expand_path(File.dirname(__FILE__))
+
+module QA
+ ##
+ # GitLab QA runtime classes, mostly singletons.
+ #
+ module Runtime
+ autoload :Release, 'qa/runtime/release'
+ autoload :User, 'qa/runtime/user'
+ autoload :Namespace, 'qa/runtime/namespace'
+ end
+
+ ##
+ # GitLab QA Scenarios
+ #
+ module Scenario
+ ##
+ # Support files
+ #
+ autoload :Actable, 'qa/scenario/actable'
+ autoload :Template, 'qa/scenario/template'
+
+ ##
+ # Test scenario entrypoints.
+ #
+ module Test
+ autoload :Instance, 'qa/scenario/test/instance'
+ end
+
+ ##
+ # GitLab instance scenarios.
+ #
+ module Gitlab
+ module Project
+ autoload :Create, 'qa/scenario/gitlab/project/create'
+ end
+ end
+ end
+
+ ##
+ # Classes describing structure of GitLab, pages, menus etc.
+ #
+ # Needed to execute click-driven-only black-box tests.
+ #
+ module Page
+ autoload :Base, 'qa/page/base'
+
+ module Main
+ autoload :Entry, 'qa/page/main/entry'
+ autoload :Menu, 'qa/page/main/menu'
+ autoload :Groups, 'qa/page/main/groups'
+ autoload :Projects, 'qa/page/main/projects'
+ end
+
+ module Project
+ autoload :New, 'qa/page/project/new'
+ autoload :Show, 'qa/page/project/show'
+ end
+
+ module Admin
+ autoload :Menu, 'qa/page/admin/menu'
+ end
+ end
+
+ ##
+ # Classes describing operations on Git repositories.
+ #
+ module Git
+ autoload :Repository, 'qa/git/repository'
+ end
+
+ ##
+ # Classes that make it possible to execute features tests.
+ #
+ module Specs
+ autoload :Config, 'qa/specs/config'
+ autoload :Runner, 'qa/specs/runner'
+ end
+end
+
+QA::Runtime::Release.extend_autoloads!
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
new file mode 100644
index 00000000000..6d1601dfa48
--- /dev/null
+++ b/qa/qa/ce/strategy.rb
@@ -0,0 +1,15 @@
+module QA
+ module CE
+ module Strategy
+ extend self
+
+ def extend_autoloads!
+ # noop
+ end
+
+ def perform_before_hooks
+ # noop
+ end
+ end
+ end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
new file mode 100644
index 00000000000..b9e199000d6
--- /dev/null
+++ b/qa/qa/git/repository.rb
@@ -0,0 +1,71 @@
+require 'uri'
+
+module QA
+ module Git
+ class Repository
+ include Scenario::Actable
+
+ def self.perform(*args)
+ Dir.mktmpdir do |dir|
+ Dir.chdir(dir) { super }
+ end
+ end
+
+ def location=(address)
+ @location = address
+ @uri = URI(address)
+ end
+
+ def username=(name)
+ @username = name
+ @uri.user = name
+ end
+
+ def password=(pass)
+ @password = pass
+ @uri.password = pass
+ end
+
+ def use_default_credentials
+ self.username = Runtime::User.name
+ self.password = Runtime::User.password
+ end
+
+ def clone(opts = '')
+ `git clone #{opts} #{@uri.to_s} ./`
+ end
+
+ def shallow_clone
+ clone('--depth 1')
+ end
+
+ def configure_identity(name, email)
+ `git config user.name #{name}`
+ `git config user.email #{email}`
+ end
+
+ def commit_file(name, contents, message)
+ add_file(name, contents)
+ commit(message)
+ end
+
+ def add_file(name, contents)
+ File.write(name, contents)
+
+ `git add #{name}`
+ end
+
+ def commit(message)
+ `git commit -m "#{message}"`
+ end
+
+ def push_changes(branch = 'master')
+ `git push #{@uri.to_s} #{branch}`
+ end
+
+ def commits
+ `git log --oneline`.split("\n")
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
new file mode 100644
index 00000000000..b01a4e10f93
--- /dev/null
+++ b/qa/qa/page/admin/menu.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Admin
+ class Menu < Page::Base
+ def go_to_license
+ within_middle_menu { click_link 'License' }
+ end
+
+ private
+
+ def within_middle_menu
+ page.within('.nav-control') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
new file mode 100644
index 00000000000..d55326c5262
--- /dev/null
+++ b/qa/qa/page/base.rb
@@ -0,0 +1,12 @@
+module QA
+ module Page
+ class Base
+ include Capybara::DSL
+ include Scenario::Actable
+
+ def refresh
+ visit current_path
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
new file mode 100644
index 00000000000..a9810beeb29
--- /dev/null
+++ b/qa/qa/page/main/entry.rb
@@ -0,0 +1,32 @@
+module QA
+ module Page
+ module Main
+ class Entry < Page::Base
+ def initialize
+ visit('/')
+
+ # This resolves cold boot / background tasks problems
+ #
+ start = Time.now
+
+ while Time.now - start < 240
+ break if page.has_css?('.application', wait: 10)
+ refresh
+ end
+ end
+
+ def sign_in_using_credentials
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb
new file mode 100644
index 00000000000..84597719a84
--- /dev/null
+++ b/qa/qa/page/main/groups.rb
@@ -0,0 +1,20 @@
+module QA
+ module Page
+ module Main
+ class Groups < Page::Base
+ def prepare_test_namespace
+ return if page.has_content?(Runtime::Namespace.name)
+
+ click_on 'New Group'
+
+ fill_in 'group_path', with: Runtime::Namespace.name
+ fill_in 'group_description',
+ with: "QA test run at #{Runtime::Namespace.time}"
+ choose 'Private'
+
+ click_button 'Create group'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
new file mode 100644
index 00000000000..45db7a92fa4
--- /dev/null
+++ b/qa/qa/page/main/menu.rb
@@ -0,0 +1,46 @@
+module QA
+ module Page
+ module Main
+ class Menu < Page::Base
+ def go_to_groups
+ within_global_menu { click_link 'Groups' }
+ end
+
+ def go_to_projects
+ within_global_menu { click_link 'Projects' }
+ end
+
+ def go_to_admin_area
+ within_user_menu { click_link 'Admin Area' }
+ end
+
+ def sign_out
+ within_user_menu do
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
+ end
+ end
+
+ def has_personal_area?
+ page.has_selector?('.header-user-dropdown-toggle')
+ end
+
+ private
+
+ def within_global_menu
+ find('.global-dropdown-toggle').click
+
+ page.within('.global-dropdown-menu') do
+ yield
+ end
+ end
+
+ def within_user_menu
+ page.within('.navbar-nav') do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb
new file mode 100644
index 00000000000..28d3a424022
--- /dev/null
+++ b/qa/qa/page/main/projects.rb
@@ -0,0 +1,16 @@
+module QA
+ module Page
+ module Main
+ class Projects < Page::Base
+ def go_to_new_project
+ ##
+ # There are 'New Project' and 'New project' buttons on the projects
+ # page, so we can't use `click_on`.
+ #
+ button = find('a', text: /^new project$/i)
+ button.click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
new file mode 100644
index 00000000000..b31bec27b59
--- /dev/null
+++ b/qa/qa/page/project/new.rb
@@ -0,0 +1,24 @@
+module QA
+ module Page
+ module Project
+ class New < Page::Base
+ def choose_test_namespace
+ find('#s2id_project_namespace_id').click
+ find('.select2-result-label', text: Runtime::Namespace.name).click
+ end
+
+ def choose_name(name)
+ fill_in 'project_path', with: name
+ end
+
+ def add_description(description)
+ fill_in 'project_description', with: description
+ end
+
+ def create_new_project
+ click_on 'Create project'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
new file mode 100644
index 00000000000..56a270d8fcc
--- /dev/null
+++ b/qa/qa/page/project/show.rb
@@ -0,0 +1,23 @@
+module QA
+ module Page
+ module Project
+ class Show < Page::Base
+ def choose_repository_clone_http
+ find('#clone-dropdown').click
+
+ page.within('#clone-dropdown') do
+ find('span', text: 'HTTP').click
+ end
+ end
+
+ def repository_location
+ find('#project_clone').value
+ end
+
+ def wait_for_push
+ sleep 5
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
new file mode 100644
index 00000000000..e4910b63a14
--- /dev/null
+++ b/qa/qa/runtime/namespace.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module Namespace
+ extend self
+
+ def time
+ @time ||= Time.now
+ end
+
+ def name
+ 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
new file mode 100644
index 00000000000..4f83a773645
--- /dev/null
+++ b/qa/qa/runtime/release.rb
@@ -0,0 +1,28 @@
+module QA
+ module Runtime
+ ##
+ # Class that is responsible for plugging CE/EE extensions in, depending on
+ # existence of EE module.
+ #
+ # We need that to reduce the probability of conflicts when merging
+ # CE to EE.
+ #
+ class Release
+ def initialize
+ require "qa/#{version.downcase}/strategy"
+ end
+
+ def version
+ @version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE
+ end
+
+ def strategy
+ QA.const_get("QA::#{version}::Strategy")
+ end
+
+ def self.method_missing(name, *args)
+ self.new.strategy.public_send(name, *args)
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
new file mode 100644
index 00000000000..12ceda015f0
--- /dev/null
+++ b/qa/qa/runtime/user.rb
@@ -0,0 +1,15 @@
+module QA
+ module Runtime
+ module User
+ extend self
+
+ def name
+ ENV['GITLAB_USERNAME'] || 'root'
+ end
+
+ def password
+ ENV['GITLAB_PASSWORD'] || 'test1234'
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb
new file mode 100644
index 00000000000..6cdbd24780e
--- /dev/null
+++ b/qa/qa/scenario/actable.rb
@@ -0,0 +1,23 @@
+module QA
+ module Scenario
+ module Actable
+ def act(*args, &block)
+ instance_exec(*args, &block)
+ end
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def perform
+ yield new if block_given?
+ end
+
+ def act(*args, &block)
+ new.act(*args, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
new file mode 100644
index 00000000000..38522714e64
--- /dev/null
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -0,0 +1,31 @@
+require 'securerandom'
+
+module QA
+ module Scenario
+ module Gitlab
+ module Project
+ class Create < Scenario::Template
+ attr_writer :description
+
+ def name=(name)
+ @name = "#{name}-#{SecureRandom.hex(8)}"
+ end
+
+ def perform
+ Page::Main::Menu.act { go_to_groups }
+ Page::Main::Groups.act { prepare_test_namespace }
+ Page::Main::Menu.act { go_to_projects }
+ Page::Main::Projects.act { go_to_new_project }
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.create_new_project
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
new file mode 100644
index 00000000000..341998af160
--- /dev/null
+++ b/qa/qa/scenario/template.rb
@@ -0,0 +1,16 @@
+module QA
+ module Scenario
+ class Template
+ def self.perform(*args)
+ new.tap do |scenario|
+ yield scenario if block_given?
+ return scenario.perform(*args)
+ end
+ end
+
+ def perform(*_args)
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
new file mode 100644
index 00000000000..689292bc60b
--- /dev/null
+++ b/qa/qa/scenario/test/instance.rb
@@ -0,0 +1,26 @@
+module QA
+ module Scenario
+ module Test
+ ##
+ # Run test suite against any GitLab instance,
+ # including staging and on-premises installation.
+ #
+ class Instance < Scenario::Template
+ def perform(address, *files)
+ Specs::Config.perform do |specs|
+ specs.address = address
+ end
+
+ ##
+ # Perform before hooks, which are different for CE and EE
+ #
+ Runtime::Release.perform_before_hooks
+
+ Specs::Runner.perform do |specs|
+ specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
new file mode 100644
index 00000000000..d72187fcd34
--- /dev/null
+++ b/qa/qa/specs/config.rb
@@ -0,0 +1,78 @@
+require 'rspec/core'
+require 'capybara/rspec'
+require 'capybara-webkit'
+require 'capybara-screenshot/rspec'
+
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+
+module QA
+ module Specs
+ class Config < Scenario::Template
+ attr_writer :address
+
+ def initialize
+ @address = ENV['GITLAB_URL']
+ end
+
+ def perform
+ raise 'Please configure GitLab address!' unless @address
+
+ configure_rspec!
+ configure_capybara!
+ configure_webkit!
+ end
+
+ def configure_rspec!
+ RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`.
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # Run specs in random order to surface order dependencies.
+ config.order = :random
+ Kernel.srand config.seed
+
+ config.before(:all) do
+ page.current_window.resize_to(1200, 1800)
+ end
+
+ config.formatter = :documentation
+ config.color = true
+ end
+ end
+
+ def configure_capybara!
+ Capybara.configure do |config|
+ config.app_host = @address
+ config.default_driver = :webkit
+ config.javascript_driver = :webkit
+ config.default_max_wait_time = 4
+
+ # https://github.com/mattheworiordan/capybara-screenshot/issues/164
+ config.save_path = 'tmp'
+ end
+ end
+
+ def configure_webkit!
+ Capybara::Webkit.configure do |config|
+ config.allow_url(@address)
+ config.block_unknown_urls
+ end
+ rescue RuntimeError # rubocop:disable Lint/HandleExceptions
+ # TODO, Webkit is already configured, this make this
+ # configuration step idempotent, should be improved.
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
new file mode 100644
index 00000000000..8e1ae6efa47
--- /dev/null
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -0,0 +1,14 @@
+module QA
+ feature 'standard root login' do
+ scenario 'user logs in using credentials' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ # TODO, since `Signed in successfully` message was removed
+ # this is the only way to tell if user is signed in correctly.
+ #
+ Page::Main::Menu.perform do |menu|
+ expect(menu).to have_personal_area
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
new file mode 100644
index 00000000000..610492b9717
--- /dev/null
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -0,0 +1,19 @@
+module QA
+ feature 'create a new project' do
+ scenario 'user creates a new project' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |project|
+ project.name = 'awesome-project'
+ project.description = 'create awesome project test'
+ end
+
+ expect(page).to have_content(
+ /Project \S?awesome-project\S+ was successfully created/
+ )
+
+ expect(page).to have_content('create awesome project test')
+ expect(page).to have_content('The repository for this project is empty')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
new file mode 100644
index 00000000000..521bd955857
--- /dev/null
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -0,0 +1,57 @@
+module QA
+ feature 'clone code from the repository' do
+ context 'with regular account over http' do
+ given(:location) do
+ Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+ end
+
+ before do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |scenario|
+ scenario.name = 'project-with-code'
+ scenario.description = 'project for git clone tests'
+ end
+
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ commit_file('test.rb', 'class Test; end', 'Add Test class')
+ commit_file('README.md', '# Test', 'Add Readme')
+ push_changes
+ end
+ end
+ end
+
+ scenario 'user performs a deep clone' do
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act { clone }
+
+ expect(repository.commits.size).to eq 2
+ end
+ end
+
+ scenario 'user performs a shallow clone' do
+ Git::Repository.perform do |repository|
+ repository.location = location
+ repository.use_default_credentials
+
+ repository.act { shallow_clone }
+
+ expect(repository.commits.size).to eq 1
+ expect(repository.commits.first).to include 'Add Readme'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
new file mode 100644
index 00000000000..5fe45d63d37
--- /dev/null
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -0,0 +1,39 @@
+module QA
+ feature 'push code to repository' do
+ context 'with regular account over http' do
+ scenario 'user pushes code to the repository' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+
+ Scenario::Gitlab::Project::Create.perform do |scenario|
+ scenario.name = 'project_with_code'
+ scenario.description = 'project with repository'
+ end
+
+ Git::Repository.perform do |repository|
+ repository.location = Page::Project::Show.act do
+ choose_repository_clone_http
+ repository_location
+ end
+
+ repository.use_default_credentials
+
+ repository.act do
+ clone
+ configure_identity('GitLab QA', 'root@gitlab.com')
+ add_file('README.md', '# This is test project')
+ commit('Add README.md')
+ push_changes
+ end
+ end
+
+ Page::Project::Show.act do
+ wait_for_push
+ refresh
+ end
+
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
new file mode 100644
index 00000000000..83ae15d0995
--- /dev/null
+++ b/qa/qa/specs/runner.rb
@@ -0,0 +1,15 @@
+require 'rspec/core'
+
+module QA
+ module Specs
+ class Runner
+ include Scenario::Actable
+
+ def rspec(*args)
+ RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+ abort if status.nonzero?
+ end
+ end
+ end
+ end
+end
diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb
new file mode 100644
index 00000000000..e6b5a8dc315
--- /dev/null
+++ b/qa/spec/runtime/release_spec.rb
@@ -0,0 +1,50 @@
+describe QA::Runtime::Release do
+ context 'when release version has extension strategy' do
+ let(:strategy) { spy('strategy') }
+
+ before do
+ stub_const('QA::CE::Strategy', strategy)
+ stub_const('QA::EE::Strategy', strategy)
+ end
+
+ describe '#version' do
+ it 'return either CE or EE version' do
+ expect(subject.version).to eq(:CE).or eq(:EE)
+ end
+ end
+
+ describe '#strategy' do
+ it 'return the strategy constant' do
+ expect(subject.strategy).to eq strategy
+ end
+ end
+
+ describe 'delegated class methods' do
+ it 'delegates all calls to strategy class' do
+ described_class.some_method(1, 2)
+
+ expect(strategy).to have_received(:some_method)
+ .with(1, 2)
+ end
+ end
+ end
+
+ context 'when release version does not have extension strategy' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:version).and_return('something')
+ end
+
+ describe '#strategy' do
+ it 'raises error' do
+ expect { subject.strategy }.to raise_error(LoadError)
+ end
+ end
+
+ describe 'delegated class methods' do
+ it 'raises error' do
+ expect { described_class.some_method(2, 3) }.to raise_error(LoadError)
+ end
+ end
+ end
+end
diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb
new file mode 100644
index 00000000000..422763910e4
--- /dev/null
+++ b/qa/spec/scenario/actable_spec.rb
@@ -0,0 +1,47 @@
+describe QA::Scenario::Actable do
+ subject do
+ Class.new do
+ include QA::Scenario::Actable
+
+ attr_accessor :something
+
+ def do_something(arg = nil)
+ "some#{arg}"
+ end
+ end
+ end
+
+ describe '.act' do
+ it 'provides means to run steps' do
+ result = subject.act { do_something }
+
+ expect(result).to eq 'some'
+ end
+
+ it 'supports passing variables' do
+ result = subject.act('thing') do |variable|
+ do_something(variable)
+ end
+
+ expect(result).to eq 'something'
+ end
+
+ it 'returns value from the last method' do
+ result = subject.act { 'test' }
+
+ expect(result).to eq 'test'
+ end
+ end
+
+ describe '.perform' do
+ it 'makes it possible to pass binding' do
+ variable = 'something'
+
+ result = subject.perform do |object|
+ object.something = variable
+ end
+
+ expect(result).to eq 'something'
+ end
+ end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
new file mode 100644
index 00000000000..c07a3234673
--- /dev/null
+++ b/qa/spec/spec_helper.rb
@@ -0,0 +1,19 @@
+require_relative '../qa'
+
+RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.verify_partial_doubles = true
+ end
+
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+ config.disable_monkey_patching!
+ config.expose_dsl_globally = true
+ config.warnings = true
+ config.profile_examples = 10
+ config.order = :random
+ Kernel.srand config.seed
+end
diff --git a/rubocop/cop/custom_error_class.rb b/rubocop/cop/custom_error_class.rb
new file mode 100644
index 00000000000..38d93acfe88
--- /dev/null
+++ b/rubocop/cop/custom_error_class.rb
@@ -0,0 +1,64 @@
+module RuboCop
+ module Cop
+ # This cop makes sure that custom error classes, when empty, are declared
+ # with Class.new.
+ #
+ # @example
+ # # bad
+ # class FooError < StandardError
+ # end
+ #
+ # # okish
+ # class FooError < StandardError; end
+ #
+ # # good
+ # FooError = Class.new(StandardError)
+ class CustomErrorClass < RuboCop::Cop::Cop
+ MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze
+
+ def on_class(node)
+ _klass, parent, body = node.children
+
+ return if body
+
+ parent_klass = class_name_from_node(parent)
+
+ return unless parent_klass && parent_klass.to_s.end_with?('Error')
+
+ add_offense(node, :expression)
+ end
+
+ def autocorrect(node)
+ klass, parent, _body = node.children
+ replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})"
+
+ lambda do |corrector|
+ corrector.replace(node.source_range, replacement)
+ end
+ end
+
+ private
+
+ # The nested constant `Foo::Bar::Baz` looks like:
+ #
+ # s(:const,
+ # s(:const,
+ # s(:const, nil, :Foo), :Bar), :Baz)
+ #
+ # So recurse through that to get the name as written in the source.
+ #
+ def class_name_from_node(node, suffix = nil)
+ return unless node&.type == :const
+
+ name = node.children[1].to_s
+ name = "#{name}::#{suffix}" if suffix
+
+ if node.children[0]
+ class_name_from_node(node.children[0], name)
+ else
+ name
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
index c199f6acab2..e157d8e0791 100644
--- a/rubocop/cop/gem_fetcher.rb
+++ b/rubocop/cop/gem_fetcher.rb
@@ -4,9 +4,9 @@ module RuboCop
# `Gemfile` in order to avoid additional points of failure beyond
# rubygems.org.
class GemFetcher < RuboCop::Cop::Cop
- MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'.freeze
- GIT_KEYS = [:git, :github]
+ GIT_KEYS = [:git, :github].freeze
def on_send(node)
return unless gemfile?(node)
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
index 1490fcdd814..d2cf36c454a 100644
--- a/rubocop/cop/migration/add_column.rb
+++ b/rubocop/cop/migration/add_column.rb
@@ -8,10 +8,10 @@ module RuboCop
class AddColumn < RuboCop::Cop::Cop
include MigrationHelpers
- WHITELISTED_TABLES = [:application_settings]
+ WHITELISTED_TABLES = [:application_settings].freeze
MSG = '`add_column` with a default value requires downtime, ' \
- 'use `add_column_with_default` instead'
+ 'use `add_column_with_default` instead'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
index 747d7caf1ef..54a920d4b49 100644
--- a/rubocop/cop/migration/add_column_with_default.rb
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers
MSG = '`add_column_with_default` is not reversible so you must manually define ' \
- 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'
+ 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
index e40a7087a47..d1fc94d55be 100644
--- a/rubocop/cop/migration/add_concurrent_foreign_key.rb
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -8,7 +8,7 @@ module RuboCop
class AddConcurrentForeignKey < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'
+ MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb
new file mode 100644
index 00000000000..332fb7dcbd7
--- /dev/null
+++ b/rubocop/cop/migration/add_concurrent_index.rb
@@ -0,0 +1,34 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_concurrent_index` is used with `up`/`down` methods
+ # and not `change`.
+ class AddConcurrentIndex < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
+ 'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_concurrent_index
+
+ node.each_ancestor(:def) do |def_node|
+ next unless method_name(def_node) == :change
+
+ add_offense(def_node, :name)
+ end
+ end
+
+ def method_name(node)
+ node.children.first
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
index 5e6766f6994..fa21a0d6555 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -7,7 +7,7 @@ module RuboCop
class AddIndex < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'
+ MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'.freeze
def on_def(node)
return unless in_migration?(node)
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index d4266d0deae..a50a522cf9d 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,5 +1,7 @@
+require_relative 'cop/custom_error_class'
require_relative 'cop/gem_fetcher'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default'
require_relative 'cop/migration/add_concurrent_foreign_key'
+require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 0b8ff006d22..092048a6259 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,20 +1,36 @@
require 'spec_helper'
describe 'mail_room.yml' do
- let(:config_path) { 'config/mail_room.yml' }
- let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
- before(:each) { clear_raw_config }
- after(:each) { clear_raw_config }
+ include StubENV
- context 'when incoming email is disabled' do
- before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s
- Gitlab::MailRoom.reset_config!
- end
+ let(:mailroom_config_path) { 'config/mail_room.yml' }
+ let(:gitlab_config_path) { 'config/mail_room.yml' }
+ let(:redis_config_path) { 'config/resque.yml' }
- after do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
- end
+ let(:configuration) do
+ vars = {
+ 'MAIL_ROOM_GITLAB_CONFIG_FILE' => absolute_path(gitlab_config_path),
+ 'GITLAB_REDIS_CONFIG_FILE' => absolute_path(redis_config_path)
+ }
+ cmd = "puts ERB.new(File.read(#{absolute_path(mailroom_config_path).inspect})).result"
+
+ output, status = Gitlab::Popen.popen(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars)
+ raise "Error interpreting #{mailroom_config_path}: #{output}" unless status.zero?
+
+ YAML.load(output)
+ end
+
+ before(:each) do
+ stub_env('GITLAB_REDIS_CONFIG_FILE', absolute_path(redis_config_path))
+ clear_redis_raw_config
+ end
+
+ after(:each) do
+ clear_redis_raw_config
+ end
+
+ context 'when incoming email is disabled' do
+ let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_disabled.yml' }
it 'contains no configuration' do
expect(configuration[:mailboxes]).to be_nil
@@ -22,21 +38,12 @@ describe 'mail_room.yml' do
end
context 'when incoming email is enabled' do
- let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
- let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
-
- before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s
- Gitlab::MailRoom.reset_config!
- end
+ let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
+ let(:redis_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
- after do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
- end
+ let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
it 'contains the intended configuration' do
- stub_const('Gitlab::Redis::CONFIG_FILE', redis_config)
-
expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
@@ -66,9 +73,13 @@ describe 'mail_room.yml' do
end
end
- def clear_raw_config
+ def clear_redis_raw_config
Gitlab::Redis.remove_instance_variable(:@_raw_config)
rescue NameError
# raised if @_raw_config was not set; ignore
end
+
+ def absolute_path(path)
+ Rails.root.join(path).to_s
+ end
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
new file mode 100644
index 00000000000..84a1ce773a1
--- /dev/null
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::ApplicationSettingsController do
+ include StubENV
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ describe 'PATCH #update' do
+ it 'updates the default_project_visibility for string value' do
+ patch :update, application_setting: { default_project_visibility: "20" }
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC
+ end
+
+ it 'falls back to default with default_project_visibility setting is omitted' do
+ patch :update, application_setting: {}
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+end
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
new file mode 100644
index 00000000000..e311b8a63b2
--- /dev/null
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Admin::ApplicationsController do
+ let(:admin) { create(:admin) }
+ let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'GET #new' do
+ it 'renders the application form' do
+ get :new
+
+ expect(response).to render_template :new
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'GET #edit' do
+ it 'renders the application form' do
+ get :edit, id: application.id
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'creates the application' do
+ expect do
+ post :create, doorkeeper_application: attributes_for(:application)
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to redirect_to(admin_application_path(application))
+ end
+
+ it 'renders the application form on errors' do
+ expect do
+ post :create, doorkeeper_application: attributes_for(:application).merge(redirect_uri: nil)
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to render_template :new
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'PATCH #update' do
+ it 'updates the application' do
+ patch :update, id: application.id, doorkeeper_application: { redirect_uri: 'http://example.com/' }
+
+ expect(response).to redirect_to(admin_application_path(application))
+ expect(application.reload.redirect_uri).to eq 'http://example.com/'
+ end
+
+ it 'renders the application form on errors' do
+ patch :update, id: application.id, doorkeeper_application: { redirect_uri: nil }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+end
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index 2fcb4a6a528..44e011fd3a8 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -19,8 +19,8 @@ describe Projects::BlobController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
@@ -50,8 +50,8 @@ describe Projects::BlobController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
controller.instance_variable_set(:@blob, nil)
end
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
deleted file mode 100644
index 86f01f437a2..00000000000
--- a/spec/controllers/ci/projects_controller_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProjectsController do
- let(:visibility) { :public }
- let!(:project) { create(:empty_project, visibility, ci_id: 1) }
- let(:ci_id) { project.ci_id }
-
- describe '#index' do
- context 'user signed in' do
- before do
- sign_in(create(:user))
- get(:index)
- end
-
- it 'redirects to /' do
- expect(response).to redirect_to(root_path)
- end
- end
-
- context 'user not signed in' do
- before { get(:index) }
-
- it 'redirects to sign in page' do
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- ##
- # Specs for *deprecated* CI badge
- #
- describe '#badge' do
- shared_examples 'badge provider' do
- it 'shows badge' do
- expect(response.status).to eq 200
- expect(response.headers)
- .to include('Content-Type' => 'image/svg+xml')
- end
- end
-
- context 'user not signed in' do
- before { get(:badge, id: ci_id) }
-
- context 'project has no ci_id reference' do
- let(:ci_id) { 123 }
-
- it 'returns 404' do
- expect(response.status).to eq 404
- end
- end
-
- context 'project is public' do
- let(:visibility) { :public }
- it_behaves_like 'badge provider'
- end
-
- context 'project is private' do
- let(:visibility) { :private }
- it_behaves_like 'badge provider'
- end
- end
-
- context 'user signed in' do
- let(:user) { create(:user) }
- before { sign_in(user) }
- before { get(:badge, id: ci_id) }
-
- context 'private is internal' do
- let(:visibility) { :internal }
- it_behaves_like 'badge provider'
- end
- end
- end
-end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 7072bd5e87c..71a4a2c43c7 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -49,4 +49,18 @@ describe Dashboard::TodosController do
expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
end
end
+
+ describe 'PATCH #bulk_restore' do
+ let(:todos) { create_list(:todo, 2, :done, user: user, project: project, author: author) }
+
+ it 'restores the todos to pending state' do
+ patch :bulk_restore, ids: todos.map(&:id)
+
+ todos.each do |todo|
+ expect(todo.reload).to be_pending
+ end
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
+ end
+ end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index cfe18dd4b6c..58c16cc57e6 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -64,8 +64,8 @@ describe HealthCheckController do
context 'when a service is down and an access token is provided' do
before do
- allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire')
- allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire')
+ allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire')
+ allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire')
end
it 'supports passing the token in the header' do
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index f7219690722..61e4fae46fb 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -3,16 +3,6 @@ require 'spec_helper'
describe Profiles::KeysController do
let(:user) { create(:user) }
- describe '#new' do
- before { sign_in(user) }
-
- it 'redirects to #index' do
- get :new
-
- expect(response).to redirect_to(profile_keys_path)
- end
- end
-
describe "#get_keys" do
describe "non existant user" do
it "does not generally work" do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
deleted file mode 100644
index 58caf7999cf..00000000000
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'spec_helper'
-
-describe Profiles::NotificationsController do
- let(:user) do
- create(:user) do |user|
- user.emails.create(email: 'original@example.com')
- user.emails.create(email: 'new@example.com')
- user.update(notification_email: 'original@example.com')
- user.save!
- end
- end
-
- describe 'GET show' do
- it 'renders' do
- sign_in(user)
-
- get :show
-
- expect(response).to render_template :show
- end
- end
-
- describe 'POST update' do
- it 'updates only permitted attributes' do
- sign_in(user)
-
- put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true }
-
- user.reload
- expect(user.notification_email).to eq('new@example.com')
- expect(user.notified_of_own_activity).to eq(true)
- expect(user.admin).to eq(false)
- expect(controller).to set_flash[:notice].to('Notification settings saved')
- end
-
- it 'shows an error message if the params are invalid' do
- sign_in(user)
-
- put :update, user: { notification_email: '' }
-
- expect(user.reload.notification_email).to eq('original@example.com')
- expect(controller).to set_flash[:alert].to('Failed to save new settings')
- end
- end
-end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index 45534a3a587..dfed1de2046 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -2,48 +2,55 @@ require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
+ let(:token_attributes) { attributes_for(:personal_access_token) }
+
+ before { sign_in(user) }
describe '#create' do
def created_token
PersonalAccessToken.order(:created_at).last
end
- before { sign_in(user) }
-
- it "allows creation of a token" do
+ it "allows creation of a token with scopes" do
name = FFaker::Product.brand
+ scopes = %w[api read_user]
- post :create, personal_access_token: { name: name }
+ post :create, personal_access_token: token_attributes.merge(scopes: scopes, name: name)
expect(created_token).not_to be_nil
expect(created_token.name).to eq(name)
- expect(created_token.expires_at).to be_nil
+ expect(created_token.scopes).to eq(scopes)
expect(PersonalAccessToken.active).to include(created_token)
end
it "allows creation of a token with an expiry date" do
- expires_at = 5.days.from_now
+ expires_at = 5.days.from_now.to_date
- post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+ post :create, personal_access_token: token_attributes.merge(expires_at: expires_at)
expect(created_token).not_to be_nil
- expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
+ expect(created_token.expires_at).to eq(expires_at)
end
+ end
- context "scopes" do
- it "allows creation of a token with scopes" do
- post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
+ describe '#index' do
+ let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+ let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
- expect(created_token).not_to be_nil
- expect(created_token.scopes).to eq(['api', 'read_user'])
- end
+ before { get :index }
- it "allows creation of a token with no scopes" do
- post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
+ it "retrieves active personal access tokens" do
+ expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token)
+ end
+
+ it "retrieves inactive personal access tokens" do
+ expect(assigns(:inactive_personal_access_tokens)).to include(inactive_personal_access_token)
+ end
- expect(created_token).not_to be_nil
- expect(created_token.scopes).to eq([])
- end
+ it "does not retrieve impersonation personal access tokens" do
+ expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token)
+ expect(assigns(:inactive_personal_access_tokens)).not_to include(impersonation_personal_access_token)
end
end
end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index addc5e7ec33..c086b386381 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::BlameController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 7d4636e98d1..ec36a64b415 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::BlobController do
render_views
def do_get(opts = {})
- params = { namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ params = { namespace_id: project.namespace,
+ project_id: project,
id: 'master/CHANGELOG' }
get :diff, params.merge(opts)
end
@@ -40,8 +40,8 @@ describe Projects::BlobController do
describe 'PUT update' do
let(:default_params) do
{
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: 'master/CHANGELOG',
target_branch: 'master',
content: 'Added changes',
@@ -96,8 +96,8 @@ describe Projects::BlobController do
context 'when editing on the fork' do
before do
- default_params[:namespace_id] = forked_project.namespace.to_param
- default_params[:project_id] = forked_project.to_param
+ default_params[:namespace_id] = forked_project.namespace
+ default_params[:project_id] = forked_project
end
it 'redirects to blob' do
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index ad15e3942a5..15667e8d4b1 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -43,6 +43,7 @@ describe Projects::Boards::IssuesController do
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
+ expect(development.issues.map(&:relative_position)).not_to include(nil)
end
end
@@ -90,7 +91,7 @@ describe Projects::Boards::IssuesController do
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list_id: list.try(:to_param)
}
@@ -146,7 +147,7 @@ describe Projects::Boards::IssuesController do
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list_id: list.to_param,
issue: { title: title },
@@ -209,7 +210,7 @@ describe Projects::Boards::IssuesController do
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: issue.to_param,
from_list_id: from_list_id,
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index b3f9f76a50c..432f3c53c90 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -47,7 +47,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
format: :json
end
@@ -104,7 +104,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list: { label_id: label_id },
format: :json
@@ -157,7 +157,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: list.to_param,
list: { position: position },
@@ -200,7 +200,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
delete :destroy, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: list.to_param,
format: :json
@@ -244,7 +244,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
post :generate, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
format: :json
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index cc19035740e..aed3a45c413 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -50,8 +50,8 @@ describe Projects::BoardsController do
end
def list_boards(format: :html)
- get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ get :index, namespace_id: project.namespace,
+ project_id: project,
format: format
end
end
@@ -100,8 +100,8 @@ describe Projects::BoardsController do
end
def read_board(board:, format: :html)
- get :show, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ get :show, namespace_id: project.namespace,
+ project_id: project,
id: board.to_param,
format: format
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 9de03876755..d20e7368086 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -22,8 +22,8 @@ describe Projects::BranchesController do
sign_in(user)
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
ref: ref
end
@@ -68,7 +68,7 @@ describe Projects::BranchesController do
describe "created from the new branch button on issues" do
let(:branch) { "1-feature-branch" }
- let!(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
before do
sign_in(user)
@@ -76,8 +76,8 @@ describe Projects::BranchesController do
it 'redirects' do
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
@@ -89,12 +89,49 @@ describe Projects::BranchesController do
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
end
+ context 'repository-less project' do
+ let(:project) { create :empty_project }
+
+ it 'redirects to newly created branch' do
+ result = { status: :success, branch: double(name: branch) }
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response).to redirect_to namespace_project_tree_path(project.namespace, project, branch)
+ end
+
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
+
+ project.services << build(:kubernetes_service)
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response.location).to include(namespace_project_new_blob_path(project.namespace, project, branch))
+ expect(response).to have_http_status(302)
+ end
+ end
+
context 'without issue feature access' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
@@ -106,8 +143,8 @@ describe Projects::BranchesController do
expect(SystemNoteService).not_to receive(:new_issue_branch)
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
end
@@ -126,8 +163,8 @@ describe Projects::BranchesController do
post :destroy,
format: :html,
id: 'foo/bar/baz',
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
expect(response).to have_http_status(303)
end
@@ -142,8 +179,8 @@ describe Projects::BranchesController do
post :destroy,
format: :js,
id: branch,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context "valid branch name, valid source" do
@@ -173,8 +210,8 @@ describe Projects::BranchesController do
describe "DELETE destroy_all_merged" do
def destroy_all_merged
delete :destroy_all_merged,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when user is allowed to push' do
@@ -207,4 +244,41 @@ describe Projects::BranchesController do
end
end
end
+
+ describe "GET index" do
+ render_views
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when rendering a JSON format' do
+ it 'filters branches by name' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :json,
+ search: 'master'
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response.length).to eq 1
+ expect(parsed_response.first).to eq 'master'
+ end
+ end
+
+ context 'show_all = true' do
+ it 'returns all the branches name' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :json,
+ show_all: true
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response.length).to eq(project.repository.branches.count)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index ebd2d0e092b..b223a22ae60 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::CommitController do
def go(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :show, params.merge(extra_params)
@@ -125,8 +125,8 @@ describe Projects::CommitController do
it 'renders it' do
get(:show,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
+ namespace_id: fork_project.namespace,
+ project_id: fork_project,
id: commit.id)
expect(response).to be_success
@@ -139,8 +139,8 @@ describe Projects::CommitController do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
get(:branches,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: commit.id)
expect(assigns(:branches)).to include("master", "feature_conflict")
@@ -152,8 +152,8 @@ describe Projects::CommitController do
context 'when target branch is not provided' do
it 'renders the 404 page' do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: commit.id)
expect(response).not_to be_success
@@ -164,9 +164,9 @@ describe Projects::CommitController do
context 'when the revert was successful' do
it 'redirects to the commits page' do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
@@ -177,18 +177,18 @@ describe Projects::CommitController do
context 'when the revert failed' do
before do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: commit.id)
end
it 'redirects to the commit page' do
# Reverting a commit that has been already reverted.
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
@@ -201,8 +201,8 @@ describe Projects::CommitController do
context 'when target branch is not provided' do
it 'renders the 404 page' do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: master_pickable_commit.id)
expect(response).not_to be_success
@@ -213,9 +213,9 @@ describe Projects::CommitController do
context 'when the cherry-pick was successful' do
it 'redirects to the commits page' do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
@@ -226,18 +226,18 @@ describe Projects::CommitController do
context 'when the cherry_pick failed' do
before do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: master_pickable_commit.id)
end
it 'redirects to the commit page' do
# Cherry-picking a commit that has been already cherry-picked.
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
+ namespace_id: project.namespace,
+ project_id: project,
+ start_branch: 'master',
id: master_pickable_commit.id)
expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
@@ -249,8 +249,8 @@ describe Projects::CommitController do
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
@@ -313,8 +313,8 @@ describe Projects::CommitController do
describe 'GET pipelines' do
def get_pipelines(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :pipelines, params.merge(extra_params)
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 54b8d1108a5..e26731fb691 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::CommitsController do
context "when the ref does not exist with the suffix" do
it "renders as atom" do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: "master.atom")
expect(response).to be_success
@@ -33,8 +33,8 @@ describe Projects::CommitsController do
allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: "master.atom")
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index e811c76fb31..15ac4e0925a 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CompareController do
it 'compare shows some diffs' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: ref_from,
to: ref_to)
@@ -25,8 +25,8 @@ describe Projects::CompareController do
it 'compare shows some diffs with ignore whitespace change option' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: '08f22f25',
to: '66eceea0',
w: 1)
@@ -43,8 +43,8 @@ describe Projects::CompareController do
describe 'non-existent refs' do
it 'uses invalid source ref' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: 'non-existent',
to: ref_to)
@@ -55,8 +55,8 @@ describe Projects::CompareController do
it 'uses invalid target ref' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: ref_from,
to: 'non-existent')
@@ -67,8 +67,8 @@ describe Projects::CompareController do
it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
post(:create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: '',
to: 'master')
@@ -77,8 +77,8 @@ describe Projects::CompareController do
it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
post(:create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: 'master',
to: '')
@@ -87,8 +87,8 @@ describe Projects::CompareController do
it 'redirects back to index when params[:from] and params[:to] are empty' do
post(:create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: '',
to: '')
@@ -99,8 +99,8 @@ describe Projects::CompareController do
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 6a6d71a16ee..6fae52edbad 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CycleAnalyticsController do
context 'with no data' do
it 'is true' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ namespace_id: project.namespace,
+ project_id: project)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(true)
@@ -32,8 +32,8 @@ describe Projects::CycleAnalyticsController do
it 'is false' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ namespace_id: project.namespace,
+ project_id: project)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(false)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 84d119f1867..83d80b376fb 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'GET #metrics' do
+ before do
+ allow(controller).to receive(:environment).and_return(environment)
+ end
+
+ context 'when environment has no metrics' do
+ before do
+ expect(environment).to receive(:metrics).and_return(nil)
+ end
+
+ it 'returns a metrics page' do
+ get :metrics, environment_params
+
+ expect(response).to be_ok
+ end
+
+ context 'when requesting metrics as JSON' do
+ it 'returns a metrics JSON document' do
+ get :metrics, environment_params(format: :json)
+
+ expect(response).to have_http_status(204)
+ expect(json_response).to eq({})
+ end
+ end
+ end
+
+ context 'when environment has some metrics' do
+ before do
+ expect(environment).to receive(:metrics).and_return({
+ success: true,
+ metrics: {},
+ last_update: 42
+ })
+ end
+
+ it 'returns a metrics JSON document' do
+ get :metrics, environment_params(format: :json)
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['metrics']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ end
+ end
+
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index a4884256c92..6a5433bcc9c 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::FindFileController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
@@ -36,8 +36,8 @@ describe Projects::FindFileController do
describe "GET #list" do
def go(format: 'json')
get :list,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id,
format: format
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index a867668d97b..8282d79298f 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -9,8 +9,8 @@ describe Projects::ForksController do
describe 'GET index' do
def get_forks
get :index,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when fork is public' do
@@ -71,8 +71,8 @@ describe Projects::ForksController do
describe 'GET new' do
def get_new
get :new,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when user is signed in' do
@@ -99,8 +99,8 @@ describe Projects::ForksController do
describe 'POST create' do
def post_create
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
namespace_key: user.namespace.id
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index bbe8e4bf6b2..e0de62e4454 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -9,23 +9,39 @@ describe Projects::GraphsController do
project.team << [user, :master]
end
- describe 'GET #languages' do
+ describe 'GET languages' do
+ it "redirects_to action charts" do
+ get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+ expect(response).to redirect_to action: :charts
+ end
+ end
+
+ describe 'GET commits' do
+ it "redirects_to action charts" do
+ get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+
+ expect(response).to redirect_to action: :charts
+ end
+ end
+
+ describe 'GET charts' do
let(:linguist_repository) do
double(languages: {
'Ruby' => 1000,
'CoffeeScript' => 350,
- 'PowerShell' => 15
+ 'NSIS' => 15
})
end
let(:expected_values) do
- ps_color = "##{Digest::SHA256.hexdigest('PowerShell')[0...6]}"
+ nsis_color = "##{Digest::SHA256.hexdigest('NSIS')[0...6]}"
[
# colors from Linguist:
- { label: "Ruby", color: "#701516", highlight: "#701516" },
- { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
+ { label: "Ruby", color: "#701516", highlight: "#701516" },
+ { label: "CoffeeScript", color: "#244776", highlight: "#244776" },
# colors from SHA256 fallback:
- { label: "PowerShell", color: ps_color, highlight: ps_color }
+ { label: "NSIS", color: nsis_color, highlight: nsis_color }
]
end
@@ -34,7 +50,7 @@ describe Projects::GraphsController do
end
it 'sets the correct colour according to language' do
- get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+ get(:charts, namespace_id: project.namespace, project_id: project, id: 'master')
expected_values.each do |val|
expect(assigns(:languages)).to include(a_hash_including(val))
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index a976a9c27ab..ca4a8e871c0 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::GroupLinksController do
describe '#create' do
shared_context 'link project to group' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_id: group.id,
link_group_access: ProjectGroupLink.default_access)
end
@@ -50,8 +50,8 @@ describe Projects::GroupLinksController do
context 'when project group id equal link group id' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_id: group2.id,
link_group_access: ProjectGroupLink.default_access)
end
@@ -69,8 +69,8 @@ describe Projects::GroupLinksController do
context 'when link group id is not present' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_access: ProjectGroupLink.default_access)
end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 2acbba469e3..7c75815f3c4 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::ImportsController do
end
it 'renders template' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to render_template :show
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'Started' }
expect(flash.now[:notice]).to eq 'Started'
end
@@ -39,13 +39,13 @@ describe Projects::ImportsController do
end
it 'renders template' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to render_template :show
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'In progress' }
expect(flash.now[:notice]).to eq 'In progress'
end
@@ -57,7 +57,7 @@ describe Projects::ImportsController do
end
it 'redirects to new_namespace_project_import_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project)
end
@@ -72,7 +72,7 @@ describe Projects::ImportsController do
it 'redirects to namespace_project_path' do
allow_any_instance_of(Project).to receive(:forked?).and_return(true)
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(flash[:notice]).to eq 'The project was successfully forked.'
expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -81,7 +81,7 @@ describe Projects::ImportsController do
context 'when project is external' do
it 'redirects to namespace_project_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(flash[:notice]).to eq 'The project was successfully imported.'
expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -97,7 +97,7 @@ describe Projects::ImportsController do
end
it 'redirects to params[:to]' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params
expect(flash[:notice]).to eq params[:notice]
expect(response).to redirect_to params[:to]
@@ -111,7 +111,7 @@ describe Projects::ImportsController do
end
it 'redirects to namespace_project_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to redirect_to namespace_project_path(project.namespace, project)
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 7871b6a9e10..57a921e3676 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::IssuesController do
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
- get :index, namespace_id: project.namespace.path, project_id: project
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to('https://example.com/project')
end
@@ -27,13 +27,13 @@ describe Projects::IssuesController do
it_behaves_like "issuables list meta-data", :issue
it "returns index" do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
end
it "returns 301 if request path doesn't match project path" do
- get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+ get :index, namespace_id: project.namespace, project_id: project.path.upcase
expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
end
@@ -42,7 +42,7 @@ describe Projects::IssuesController do
project.issues_enabled = false
project.save
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
end
@@ -50,7 +50,7 @@ describe Projects::IssuesController do
controller.instance_variable_set(:@project, project)
allow(project).to receive(:default_issues_tracker?).and_return(false)
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
end
end
@@ -67,8 +67,8 @@ describe Projects::IssuesController do
it 'redirects to last_page if page number is larger than number of pages' do
get :index,
- namespace_id: project.namespace.path.to_param,
- project_id: project.path.to_param,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
page: (last_page + 1).to_param
expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
@@ -76,8 +76,8 @@ describe Projects::IssuesController do
it 'redirects to specified page' do
get :index,
- namespace_id: project.namespace.path.to_param,
- project_id: project.path.to_param,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
page: last_page.to_param
expect(assigns(:issues).current_page).to eq(last_page)
@@ -87,6 +87,12 @@ describe Projects::IssuesController do
end
describe 'GET #new' do
+ it 'redirects to signin if not logged in' do
+ get :new, namespace_id: project.namespace, project_id: project
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+
context 'internal issue tracker' do
before do
sign_in(user)
@@ -94,7 +100,7 @@ describe Projects::IssuesController do
end
it 'builds a new issue' do
- get :new, namespace_id: project.namespace.path, project_id: project
+ get :new, namespace_id: project.namespace, project_id: project
expect(assigns(:issue)).to be_a_new(Issue)
end
@@ -104,7 +110,16 @@ describe Projects::IssuesController do
project_with_repository.team << [user, :developer]
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
- get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
+ get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_to_resolve_discussions_of: mr.iid
+
+ expect(assigns(:issue).title).not_to be_empty
+ expect(assigns(:issue).description).not_to be_empty
+ end
+
+ it 'fills in an issue for a discussion' do
+ note = create(:note_on_merge_request, project: project)
+
+ get :new, namespace_id: project.namespace.path, project_id: project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id
expect(assigns(:issue).title).not_to be_empty
expect(assigns(:issue).description).not_to be_empty
@@ -112,12 +127,17 @@ describe Projects::IssuesController do
end
context 'external issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
it 'redirects to the external issue tracker' do
external = double(new_issue_path: 'https://example.com/issues/new')
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
- get :new, namespace_id: project.namespace.path, project_id: project
+ get :new, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to('https://example.com/issues/new')
end
@@ -125,13 +145,33 @@ describe Projects::IssuesController do
end
describe 'PUT #update' do
- context 'when moving issue to another private project' do
- let(:another_project) { create(:empty_project, :private) }
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- before do
- sign_in(user)
- project.team << [user, :developer]
+ it_behaves_like 'update invalid issuable', Issue
+
+ context 'changing the assignee' do
+ it 'limits the attributes exposed on the assignee' do
+ assignee = create(:user)
+ project.add_developer(assignee)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: issue.iid,
+ issue: { assignee_id: assignee.id },
+ format: :json
+ body = JSON.parse(response.body)
+
+ expect(body['assignee'].keys)
+ .to match_array(%w(name username avatar_url))
end
+ end
+
+ context 'when moving issue to another private project' do
+ let(:another_project) { create(:empty_project, :private) }
context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] }
@@ -251,7 +291,7 @@ describe Projects::IssuesController do
def update_issue(issue_params = {}, additional_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: issue.iid,
issue: issue_params
}.merge(additional_params)
@@ -262,7 +302,7 @@ describe Projects::IssuesController do
def move_issue
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: issue.iid,
issue: { title: 'New title' },
move_to_project_id: another_project.id
@@ -342,7 +382,7 @@ describe Projects::IssuesController do
def get_issues
get :index,
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
end
end
@@ -405,7 +445,7 @@ describe Projects::IssuesController do
def go(id:)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id
end
end
@@ -416,7 +456,7 @@ describe Projects::IssuesController do
def go(id:)
get :edit,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id
end
end
@@ -427,7 +467,7 @@ describe Projects::IssuesController do
def go(id:)
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id,
issue: { title: 'New title' }
end
@@ -442,7 +482,7 @@ describe Projects::IssuesController do
post :create, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
}.merge(additional_params)
@@ -460,11 +500,11 @@ describe Projects::IssuesController do
end
let(:merge_request_params) do
- { merge_request_for_resolving_discussions: merge_request.iid }
+ { merge_request_to_resolve_discussions_of: merge_request.iid }
end
- def post_issue(issue_params)
- post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
+ def post_issue(issue_params, other_params: {})
+ post :create, { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params)
end
it 'creates an issue for the project' do
@@ -483,6 +523,27 @@ describe Projects::IssuesController do
expect(discussion.resolved?).to eq(true)
end
+
+ it 'sets a flash message' do
+ post_issue(title: 'Hello')
+
+ expect(flash[:notice]).to eq('Resolved all discussions.')
+ end
+
+ describe "resolving a single discussion" do
+ before do
+ post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id })
+ end
+ it 'resolves a single discussion' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+
+ it 'sets a flash message that one discussion was resolved' do
+ expect(flash[:notice]).to eq('Resolved 1 discussion.')
+ end
+ end
end
context 'Akismet is enabled' do
@@ -607,8 +668,8 @@ describe Projects::IssuesController do
project.team << [admin, :master]
sign_in(admin)
post :mark_as_spam, {
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: issue.iid
}
end
@@ -624,7 +685,7 @@ describe Projects::IssuesController do
context "when the user is a developer" do
before { sign_in(user) }
it "rejects a developer to destroy an issue" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(404)
end
end
@@ -637,7 +698,7 @@ describe Projects::IssuesController do
before { sign_in(owner) }
it "deletes the issue" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
@@ -646,7 +707,7 @@ describe Projects::IssuesController do
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
end
end
end
@@ -659,8 +720,8 @@ describe Projects::IssuesController do
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: issue.iid, name: "thumbsup")
+ post(:toggle_award_emoji, namespace_id: project.namespace,
+ project_id: project, id: issue.iid, name: "thumbsup")
end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3e0326dd47d..6a6e9bf378a 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -67,7 +67,7 @@ describe Projects::LabelsController do
end
def list_labels
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
@@ -76,7 +76,7 @@ describe Projects::LabelsController do
let(:personal_project) { create(:empty_project, namespace: user.namespace) }
it 'creates labels' do
- post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+ post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project
expect(response).to have_http_status(302)
end
@@ -84,7 +84,7 @@ describe Projects::LabelsController do
context 'project belonging to a group' do
it 'creates labels' do
- post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+ post :generate, namespace_id: project.namespace.to_param, project_id: project
expect(response).to have_http_status(302)
end
@@ -109,7 +109,7 @@ describe Projects::LabelsController do
end
def toggle_subscription(label)
- post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
+ post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project, id: label.to_param
end
end
@@ -119,7 +119,7 @@ describe Projects::LabelsController do
context 'not group owner' do
it 'denies access' do
- post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(response).to have_http_status(404)
end
@@ -131,13 +131,13 @@ describe Projects::LabelsController do
end
it 'gives access' do
- post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(response).to redirect_to(namespace_project_labels_path)
end
it 'promotes the label' do
- post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(Label.where(id: label_1.id)).to be_empty
expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil
@@ -151,7 +151,7 @@ describe Projects::LabelsController do
end
it 'returns to label list' do
- post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(response).to redirect_to(namespace_project_labels_path)
end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index cae733f0cfb..c5abf11cfa5 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::MattermostsController do
it 'accepts the request' do
get(:new,
namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ project_id: project)
expect(response).to have_http_status(200)
end
@@ -30,7 +30,7 @@ describe Projects::MattermostsController do
subject do
post(:create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
mattermost: mattermost_params)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index f84f922ba5e..c310d830e81 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -43,7 +43,8 @@ describe Projects::MergeRequestsController do
submit_new_merge_request(format: :json)
expect(response).to be_ok
- expect(json_response).not_to be_empty
+ expect(json_response).to have_key 'pipelines'
+ expect(json_response['pipelines']).not_to be_empty
end
end
end
@@ -51,10 +52,11 @@ describe Projects::MergeRequestsController do
def submit_new_merge_request(format: :html)
get :new,
namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
+ project_id: fork_project,
merge_request: {
source_branch: 'remove-submodule',
- target_branch: 'master' },
+ target_branch: 'master'
+ },
format: format
end
end
@@ -63,7 +65,7 @@ describe Projects::MergeRequestsController do
it "loads labels into the @labels variable" do
get action,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: 'html'
expect(assigns(:labels)).not_to be_nil
@@ -75,7 +77,7 @@ describe Projects::MergeRequestsController do
it "does generally work" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -89,7 +91,7 @@ describe Projects::MergeRequestsController do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
end
@@ -97,7 +99,7 @@ describe Projects::MergeRequestsController do
it "renders it" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -110,7 +112,7 @@ describe Projects::MergeRequestsController do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -125,7 +127,7 @@ describe Projects::MergeRequestsController do
it "triggers workhorse to serve the request" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :diff)
@@ -137,7 +139,7 @@ describe Projects::MergeRequestsController do
it 'triggers workhorse to serve the request' do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :patch)
@@ -152,7 +154,7 @@ describe Projects::MergeRequestsController do
def get_merge_requests(page = nil)
get :index,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
state: 'opened', page: page.to_param
end
@@ -201,6 +203,24 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ context 'changing the assignee' do
+ it 'limits the attributes exposed on the assignee' do
+ assignee = create(:user)
+ project.add_developer(assignee)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: { assignee_id: assignee.id },
+ format: :json
+ body = JSON.parse(response.body)
+
+ expect(body['assignee'].keys)
+ .to match_array(%w(name username avatar_url))
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
@@ -215,8 +235,8 @@ describe Projects::MergeRequestsController do
it 'closes MR without errors' do
post :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
state_event: 'close'
@@ -230,8 +250,8 @@ describe Projects::MergeRequestsController do
merge_request.close!
put :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
title: 'New title'
@@ -245,8 +265,8 @@ describe Projects::MergeRequestsController do
merge_request.close!
put :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
target_branch: 'new_branch'
@@ -254,14 +274,16 @@ describe Projects::MergeRequestsController do
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
+
+ it_behaves_like 'update invalid issuable', MergeRequest
end
end
describe 'POST merge' do
let(:base_params) do
{
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
format: 'raw'
}
@@ -316,41 +338,41 @@ describe Projects::MergeRequestsController do
merge_with_sha
end
- context 'when merge_when_build_succeeds is passed' do
- def merge_when_build_succeeds
- post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1')
+ context 'when the pipeline succeeds is passed' do
+ def merge_when_pipeline_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end
before do
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
end
- it 'returns :merge_when_build_succeeds' do
- merge_when_build_succeeds
+ it 'returns :merge_when_pipeline_succeeds' do
+ merge_when_pipeline_succeeds
- expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
end
- it 'sets the MR to merge when the build succeeds' do
- service = double(:merge_when_build_succeeds_service)
+ it 'sets the MR to merge when the pipeline succeeds' do
+ service = double(:merge_when_pipeline_succeeds_service)
expect(MergeRequests::MergeWhenPipelineSucceedsService)
.to receive(:new).with(project, anything, anything)
.and_return(service)
expect(service).to receive(:execute).with(merge_request)
- merge_when_build_succeeds
+ merge_when_pipeline_succeeds
end
- context 'when project.only_allow_merge_if_build_succeeds? is true' do
+ context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
before do
- project.update_column(:only_allow_merge_if_build_succeeds, true)
+ project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end
- it 'returns :merge_when_build_succeeds' do
- merge_when_build_succeeds
+ it 'returns :merge_when_pipeline_succeeds' do
+ merge_when_pipeline_succeeds
- expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
end
end
end
@@ -425,7 +447,7 @@ describe Projects::MergeRequestsController do
describe "DELETE destroy" do
it "denies access to users unless they're admin or project owner" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
expect(response).to have_http_status(404)
end
@@ -438,7 +460,7 @@ describe Projects::MergeRequestsController do
before { sign_in owner }
it "deletes the merge request" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
@@ -447,7 +469,7 @@ describe Projects::MergeRequestsController do
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
end
end
end
@@ -456,7 +478,7 @@ describe Projects::MergeRequestsController do
def go(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid
}
@@ -536,7 +558,7 @@ describe Projects::MergeRequestsController do
def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
@@ -600,7 +622,7 @@ describe Projects::MergeRequestsController do
before do
other_project.team << [user, :master]
- diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project)
end
it 'returns a 404' do
@@ -666,7 +688,7 @@ describe Projects::MergeRequestsController do
def go(format: 'html')
get :commits,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format
end
@@ -706,7 +728,7 @@ describe Projects::MergeRequestsController do
before do
get :pipelines,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :json
end
@@ -725,7 +747,7 @@ describe Projects::MergeRequestsController do
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json'
end
@@ -743,7 +765,7 @@ describe Projects::MergeRequestsController do
before do
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json'
end
@@ -772,7 +794,7 @@ describe Projects::MergeRequestsController do
section['lines'].each do |line|
if section['conflict']
- expect(line['type']).to be_in(['old', 'new'])
+ expect(line['type']).to be_in(%w(old new))
expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
else
if line['type'].nil?
@@ -806,7 +828,7 @@ describe Projects::MergeRequestsController do
post :remove_wip,
namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project.to_param,
+ project_id: merge_request.project,
id: merge_request.iid
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
@@ -817,7 +839,7 @@ describe Projects::MergeRequestsController do
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
old_path: path,
new_path: path,
@@ -873,7 +895,7 @@ describe Projects::MergeRequestsController do
def resolve_conflicts(files)
post :resolve_conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json',
files: files,
@@ -1024,7 +1046,7 @@ describe Projects::MergeRequestsController do
post :assign_related_issues,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid
end
@@ -1079,7 +1101,7 @@ describe Projects::MergeRequestsController do
get :ci_environments_status,
namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project.to_param,
+ project_id: merge_request.project,
id: merge_request.iid, format: 'json'
end
@@ -1092,8 +1114,8 @@ describe Projects::MergeRequestsController do
describe 'GET merge_widget_refresh' do
let(:params) do
{
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
format: :raw
}
@@ -1131,14 +1153,14 @@ describe Projects::MergeRequestsController do
end
context 'when waiting for build' do
- let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
- it 'sets status to :merge_when_build_succeeds' do
- expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ it 'sets status to :merge_when_pipeline_succeeds' do
+ expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
expect(response).to render_template('merge')
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index dc597202050..d80780b1d90 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -200,4 +200,31 @@ describe Projects::NotesController do
end
end
end
+
+ describe 'GET index' do
+ let(:last_fetched_at) { '1487756246' }
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'issue',
+ target_id: issue.id
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'passes last_fetched_at from headers to NotesFinder' do
+ request.headers['X-Last-Fetched-At'] = last_fetched_at
+
+ expect(NotesFinder).to receive(:new)
+ .with(anything, anything, hash_including(last_fetched_at: last_fetched_at))
+ .and_call_original
+
+ get :index, request_params
+ end
+ end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1ed2ee3ab4a..04bb5cbbd59 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -12,10 +12,13 @@ describe Projects::PipelinesController do
describe 'GET index.json' do
before do
- create_list(:ci_empty_pipeline, 2, project: project)
+ create(:ci_empty_pipeline, status: 'pending', project: project)
+ create(:ci_empty_pipeline, status: 'running', project: project)
+ create(:ci_empty_pipeline, status: 'created', project: project)
+ create(:ci_empty_pipeline, status: 'success', project: project)
- get :index, namespace_id: project.namespace.path,
- project_id: project.path,
+ get :index, namespace_id: project.namespace,
+ project_id: project,
format: :json
end
@@ -23,9 +26,11 @@ describe Projects::PipelinesController do
expect(response).to have_http_status(:ok)
expect(json_response).to include('pipelines')
- expect(json_response['pipelines'].count).to eq 2
- expect(json_response['count']['all']).to eq 2
- expect(json_response['count']['running_or_pending']).to eq 2
+ expect(json_response['pipelines'].count).to eq 4
+ expect(json_response['count']['all']).to eq 4
+ expect(json_response['count']['running']).to eq 1
+ expect(json_response['count']['pending']).to eq 1
+ expect(json_response['count']['finished']).to eq 1
end
end
@@ -57,8 +62,8 @@ describe Projects::PipelinesController do
end
def get_stage(name)
- get :stage, namespace_id: project.namespace.path,
- project_id: project.path,
+ get :stage, namespace_id: project.namespace,
+ project_id: project,
id: pipeline.id,
stage: name,
format: :json
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index da6112a13f7..e378b5714fe 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::ProtectedBranchesController do
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
it "redirects empty repo to projects page" do
- get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
+ get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b23d6e257ba..952071af57f 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -3,21 +3,21 @@ require 'spec_helper'
describe Projects::RawController do
let(:public_project) { create(:project, :public, :repository) }
- describe "#show" do
+ describe '#show' do
context 'regular filename' do
let(:id) { 'master/README.md' }
it 'delivers ASCII file' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).
- to eq("inline")
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+ to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
@@ -27,12 +27,12 @@ describe Projects::RawController do
it 'sets image content type header' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
@@ -40,32 +40,57 @@ describe Projects::RawController do
let(:id) { 'be93687/files/lfs/lfs_object.iso' }
let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
- context 'when project has access' do
+ context 'when lfs is enabled' do
before do
- public_project.lfs_objects << lfs_object
- allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.head :ok }
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
end
- it 'serves the file' do
- expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment')
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
- id: id)
+ context 'when project has access' do
+ before do
+ public_project.lfs_objects << lfs_object
+ allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+ allow(controller).to receive(:send_file) { controller.head :ok }
+ end
- expect(response).to have_http_status(200)
+ it 'serves the file' do
+ expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project,
+ id: id)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when project does not have access' do
+ it 'does not serve the file' do
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project,
+ id: id)
+
+ expect(response).to have_http_status(404)
+ end
end
end
- context 'when project does not have access' do
- it 'does not serve the file' do
+ context 'when lfs is not enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ end
+
+ it 'delivers ASCII file' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition']).
+ to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index d8fb4667c67..3a3e7467ef2 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::RefsController do
def default_get(format = :html)
get :logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: 'master',
path: 'foo/bar/baz.html',
format: format
@@ -23,7 +23,7 @@ describe Projects::RefsController do
xhr :get,
:logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project.to_param, id: 'master',
+ project_id: project, id: 'master',
path: 'foo/bar/baz.html', format: format
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 69fcc26c77e..358f26dfb02 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ReleasesController do
tag_id = release.tag
project.releases.destroy_all
- get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+ get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id
release = assigns(:release)
expect(release).not_to be_nil
@@ -24,7 +24,7 @@ describe Projects::ReleasesController do
end
it 'retrieves an existing release' do
- get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+ get :edit, namespace_id: project.namespace, project_id: project, tag_id: release.tag
release = assigns(:release)
expect(release).not_to be_nil
@@ -48,7 +48,7 @@ describe Projects::ReleasesController do
def update_release(description)
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
tag_id: release.tag,
release: { description: description }
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 04e88879fb8..9c55d159fa0 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::RepositoriesController do
describe "GET archive" do
context 'as a guest' do
it 'responds with redirect in correct format' do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, format: "zip"
expect(response.header["Content-Type"]).to start_with('text/html')
expect(response).to be_redirect
@@ -22,7 +22,7 @@ describe Projects::RepositoriesController do
end
it "uses Gitlab::Workhorse" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
@@ -33,7 +33,7 @@ describe Projects::RepositoriesController do
end
it "renders Not Found" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
expect(response).to have_http_status(404)
end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
new file mode 100644
index 00000000000..f73471f8ca8
--- /dev/null
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::Settings::RepositoryController do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 8bab094a79e..24a59caff4e 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -17,16 +17,16 @@ describe Projects::SnippetsController do
it 'redirects to last_page if page number is larger than number of pages' do
get :index,
- namespace_id: project.namespace.path,
- project_id: project.path, page: (last_page + 1).to_param
+ namespace_id: project.namespace,
+ project_id: project, page: (last_page + 1).to_param
expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
end
it 'redirects to specified page' do
get :index,
- namespace_id: project.namespace.path,
- project_id: project.path, page: last_page.to_param
+ namespace_id: project.namespace,
+ project_id: project, page: last_page.to_param
expect(assigns(:snippets).current_page).to eq(last_page)
expect(response).to have_http_status(200)
@@ -38,7 +38,7 @@ describe Projects::SnippetsController do
context 'when anonymous' do
it 'does not include the private snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).not_to include(project_snippet)
expect(response).to have_http_status(200)
@@ -49,7 +49,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'renders the snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
expect(response).to have_http_status(200)
@@ -60,7 +60,7 @@ describe Projects::SnippetsController do
before { sign_in(user2) }
it 'renders the snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
expect(response).to have_http_status(200)
@@ -77,7 +77,7 @@ describe Projects::SnippetsController do
post :create, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}.merge(additional_params)
end
@@ -152,7 +152,7 @@ describe Projects::SnippetsController do
put :update, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: snippet.id,
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}.merge(additional_params)
@@ -281,8 +281,8 @@ describe Projects::SnippetsController do
sign_in(admin)
post :mark_as_spam,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: snippet.id
end
@@ -300,7 +300,7 @@ describe Projects::SnippetsController do
context 'when anonymous' do
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(response).to have_http_status(404)
end
@@ -310,7 +310,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'renders the snippet' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response).to have_http_status(200)
@@ -321,7 +321,7 @@ describe Projects::SnippetsController do
before { sign_in(user2) }
it 'renders the snippet' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response).to have_http_status(200)
@@ -332,7 +332,7 @@ describe Projects::SnippetsController do
context 'when the project snippet does not exist' do
context 'when anonymous' do
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+ get action, namespace_id: project.namespace, project_id: project, id: 42
expect(response).to have_http_status(404)
end
@@ -342,7 +342,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+ get action, namespace_id: project.namespace, project_id: project, id: 42
expect(response).to have_http_status(404)
end
@@ -364,8 +364,8 @@ describe Projects::SnippetsController do
context 'CRLF line ending' do
let(:params) do
{
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: project_snippet.to_param
}
end
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index c36a5fdd66c..fc97bac64cd 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::TagsController do
let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
describe 'GET index' do
- before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param }
+ before { get :index, namespace_id: project.namespace.to_param, project_id: project }
it 'returns the tags for the page' do
expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
@@ -19,7 +19,7 @@ describe Projects::TagsController do
end
describe 'GET show' do
- before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+ before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id }
context "valid tag" do
let(:id) { 'v1.0.0' }
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 80f84a388ce..70e7f9ca96e 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -14,13 +14,13 @@ describe Projects::TemplatesController do
before do
project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, 'something valid',
- message: 'test 3', branch_name: 'master', update: false)
+ project.repository.create_file(user, file_path_1, 'something valid',
+ message: 'test 3', branch_name: 'master')
end
describe '#show' do
it 'renders template name and content as json' do
- get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(200)
expect(body["name"]).to eq("bug")
@@ -29,21 +29,21 @@ describe Projects::TemplatesController do
it 'renders 404 when unauthorized' do
sign_in(user2)
- get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(404)
end
it 'renders 404 when template type is not found' do
sign_in(user)
- get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(404)
end
it 'renders 404 without errors' do
sign_in(user)
- expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+ expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json) }.not_to raise_error
end
end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 415c264e0dd..9a7beeff6fe 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -12,8 +12,8 @@ describe Projects::TodosController do
describe 'POST create' do
def go
post :create,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
issuable_id: issue.id,
issuable_type: 'issue',
format: 'html'
@@ -80,8 +80,8 @@ describe Projects::TodosController do
describe 'POST create' do
def go
post :create,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
issuable_id: merge_request.id,
issuable_type: 'merge_request',
format: 'html'
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index b81645a3d2d..ab94e292e48 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::TreeController do
before do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id)
end
@@ -74,7 +74,7 @@ describe Projects::TreeController do
before do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id)
end
@@ -94,7 +94,7 @@ describe Projects::TreeController do
before do
post(:create_dir,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: 'master',
dir_name: path,
target_branch: target_branch,
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 0347e789576..cd6961a7bd5 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::UploadsController do
it "returns an error" do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
format: :json
expect(response).to have_http_status(422)
end
@@ -26,7 +26,7 @@ describe Projects::UploadsController do
before do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
file: jpg,
format: :json
end
@@ -35,13 +35,26 @@ describe Projects::UploadsController do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
end
+
+ # NOTE: This is as close as we're getting to an Integration test for this
+ # behavior. We're avoiding a proper Feature test because those should be
+ # testing things entirely user-facing, which the Upload model is very much
+ # not.
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq project
+ end
+ end
end
context 'with valid non-image file' do
before do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
file: txt,
format: :json
end
@@ -57,7 +70,7 @@ describe Projects::UploadsController do
let(:go) do
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
secret: "123456",
filename: "image.jpg"
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 9fa358f7d62..1ecfe48475c 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::VariablesController do
describe 'POST #create' do
context 'variable is valid' do
it 'shows a success flash message' do
- post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "one", value: "two" }
expect(flash[:notice]).to include 'Variables were successfully updated.'
@@ -22,7 +22,7 @@ describe Projects::VariablesController do
context 'variable is invalid' do
it 'shows an alert flash message' do
- post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "..one", value: "two" }
expect(response).to render_template("projects/variables/show")
@@ -35,12 +35,12 @@ describe Projects::VariablesController do
context 'updating a variable with valid characters' do
before do
- variable.gl_project_id = project.id
+ variable.project_id = project.id
project.variables << variable
end
it 'shows a success flash message' do
- post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: variable.key, value: 'two' }
expect(flash[:notice]).to include 'Variable was successfully updated.'
@@ -48,7 +48,7 @@ describe Projects::VariablesController do
end
it 'renders the action #show if the variable key is invalid' do
- post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: '?', value: variable.value }
expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e7aa8745b99..a88ffc1ea6a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -35,7 +35,7 @@ describe ProjectsController do
let(:private_project) { create(:empty_project, :private) }
it "does not initialize notification setting" do
- get :show, namespace_id: private_project.namespace.path, id: private_project.path
+ get :show, namespace_id: private_project.namespace, id: private_project
expect(assigns(:notification_setting)).to be_nil
end
end
@@ -43,7 +43,7 @@ describe ProjectsController do
context "user has access to project" do
context "and does not have notification setting" do
it "initializes notification as disabled" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:notification_setting).level).to eq("global")
end
end
@@ -56,7 +56,7 @@ describe ProjectsController do
end
it "shows current notification setting" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:notification_setting).level).to eq("watch")
end
end
@@ -71,24 +71,26 @@ describe ProjectsController do
end
it 'shows wiki homepage' do
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template('projects/_wiki')
end
it 'shows issues list page if wiki is disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ create(:issue, project: project)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template('projects/issues/_issues')
+ expect(assigns(:issuable_meta_data)).not_to be_nil
end
it 'shows customize workflow page if wiki and issues are disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template("projects/_customize_workflow")
end
@@ -96,7 +98,7 @@ describe ProjectsController do
it 'shows activity if enabled by user' do
user.update_attribute(:project_view, 'activity')
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template("projects/_activity")
end
@@ -113,7 +115,7 @@ describe ProjectsController do
before do
user.update_attributes(project_view: project_view)
- get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ get :show, namespace_id: empty_project.namespace, id: empty_project
end
it "renders the empty project view" do
@@ -133,7 +135,7 @@ describe ProjectsController do
before do
user.update_attributes(project_view: project_view)
- get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ get :show, namespace_id: empty_project.namespace, id: empty_project
end
it "renders the empty project view" do
@@ -154,23 +156,15 @@ describe ProjectsController do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('activity')
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_activity')
end
- it "renders the readme view" do
- allow(controller).to receive(:current_user).and_return(user)
- allow(user).to receive(:project_view).and_return('readme')
-
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
- expect(response).to render_template('_readme')
- end
-
it "renders the files view" do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('files')
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_files')
end
end
@@ -178,7 +172,7 @@ describe ProjectsController do
context "when requested with case sensitive namespace and project path" do
context "when there is a match with the same casing" do
it "loads the project" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:project)).to eq(public_project)
expect(response).to have_http_status(200)
@@ -187,10 +181,10 @@ describe ProjectsController do
context "when there is a match with different casing" do
it "redirects to the normalized path" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+ get :show, namespace_id: public_project.namespace, id: public_project.path.upcase
expect(assigns(:project)).to eq(public_project)
- expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ expect(response).to redirect_to("/#{public_project.full_path}")
end
end
end
@@ -208,7 +202,7 @@ describe ProjectsController do
project = create(:empty_project, pending_delete: true)
sign_in(user)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response.status).to eq 404
end
@@ -218,7 +212,7 @@ describe ProjectsController do
it 'redirects to project page (format.html)' do
project = create(:project, :public)
- get :show, namespace_id: project.namespace.path, id: project.path, format: :git
+ get :show, namespace_id: project.namespace, id: project, format: :git
expect(response).to have_http_status(302)
expect(response).to redirect_to(namespace_project_path)
@@ -239,7 +233,7 @@ describe ProjectsController do
sign_in(admin)
put :update,
- namespace_id: project.namespace.to_param,
+ namespace_id: project.namespace,
id: project.id,
project: project_params
@@ -257,7 +251,7 @@ describe ProjectsController do
sign_in(admin)
orig_id = project.id
- delete :destroy, namespace_id: project.namespace.path, id: project.path
+ delete :destroy, namespace_id: project.namespace, id: project
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(302)
@@ -277,7 +271,7 @@ describe ProjectsController do
project.merge_requests << merge_request
sign_in(admin)
- delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path
+ delete :destroy, namespace_id: fork_project.namespace, id: fork_project
expect(merge_request.reload.state).to eq('closed')
end
@@ -287,8 +281,8 @@ describe ProjectsController do
describe 'PUT #new_issue_address' do
subject do
put :new_issue_address,
- namespace_id: project.namespace.to_param,
- id: project.to_param
+ namespace_id: project.namespace,
+ id: project
user.reload
end
@@ -316,23 +310,23 @@ describe ProjectsController do
sign_in(user)
expect(user.starred?(public_project)).to be_falsey
post(:toggle_star,
- namespace_id: public_project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: public_project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_truthy
post(:toggle_star,
- namespace_id: public_project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: public_project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
end
it "does nothing if user is not signed in" do
post(:toggle_star,
- namespace_id: project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
post(:toggle_star,
- namespace_id: project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
end
end
@@ -366,8 +360,8 @@ describe ProjectsController do
it 'does nothing if project was not forked' do
delete(:remove_fork,
- namespace_id: unforked_project.namespace.to_param,
- id: unforked_project.to_param, format: :js)
+ namespace_id: unforked_project.namespace,
+ id: unforked_project, format: :js)
expect(flash[:notice]).to be_nil
expect(response).to render_template(:remove_fork)
@@ -377,8 +371,8 @@ describe ProjectsController do
it "does nothing if user is not signed in" do
delete(:remove_fork,
- namespace_id: project.namespace.to_param,
- id: project.to_param, format: :js)
+ namespace_id: project.namespace,
+ id: project, format: :js)
expect(response).to have_http_status(401)
end
end
@@ -387,7 +381,7 @@ describe ProjectsController do
let(:public_project) { create(:project, :public) }
it "gets a list of branches and tags" do
- get :refs, namespace_id: public_project.namespace.path, id: public_project.path
+ get :refs, namespace_id: public_project.namespace, id: public_project
parsed_body = JSON.parse(response.body)
expect(parsed_body["Branches"]).to include("master")
@@ -396,7 +390,7 @@ describe ProjectsController do
end
it "gets a list of branches, tags and commits" do
- get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+ get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456"
parsed_body = JSON.parse(response.body)
expect(parsed_body["Branches"]).to include("master")
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index b14d275f7fa..b32eb39b1fb 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -2,6 +2,26 @@ require 'spec_helper'
describe RootController do
describe 'GET index' do
+ context 'when user is not logged in' do
+ it 'redirects to the sign-in page' do
+ get :index
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+
+ context 'when a custom home page URL is defined' do
+ before do
+ stub_application_setting(home_page_url: 'https://gitlab.com')
+ end
+
+ it 'redirects the user to the custom home page URL' do
+ get :index
+
+ expect(response).to redirect_to('https://gitlab.com')
+ end
+ end
+ end
+
context 'with a user' do
let(:user) { create(:user) }
@@ -12,55 +32,60 @@ describe RootController do
context 'who has customized their dashboard setting for starred projects' do
before do
- user.update_attribute(:dashboard, 'stars')
+ user.dashboard = 'stars'
end
it 'redirects to their specified dashboard' do
get :index
+
expect(response).to redirect_to starred_dashboard_projects_path
end
end
context 'who has customized their dashboard setting for project activities' do
before do
- user.update_attribute(:dashboard, 'project_activity')
+ user.dashboard = 'project_activity'
end
it 'redirects to the activity list' do
get :index
+
expect(response).to redirect_to activity_dashboard_path
end
end
context 'who has customized their dashboard setting for starred project activities' do
before do
- user.update_attribute(:dashboard, 'starred_project_activity')
+ user.dashboard = 'starred_project_activity'
end
it 'redirects to the activity list' do
get :index
+
expect(response).to redirect_to activity_dashboard_path(filter: 'starred')
end
end
context 'who has customized their dashboard setting for groups' do
before do
- user.update_attribute(:dashboard, 'groups')
+ user.dashboard = 'groups'
end
it 'redirects to their group list' do
get :index
+
expect(response).to redirect_to dashboard_groups_path
end
end
context 'who has customized their dashboard setting for todos' do
before do
- user.update_attribute(:dashboard, 'todos')
+ user.dashboard = 'todos'
end
it 'redirects to their todo list' do
get :index
+
expect(response).to redirect_to dashboard_todos_path
end
end
@@ -68,6 +93,7 @@ describe RootController do
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
+
expect(response).to render_template 'dashboard/projects/index'
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index b56c7880b64..a06c29dd91a 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -25,9 +25,17 @@ describe SessionsController do
expect(subject.current_user). to eq user
end
- it "creates an audit log record" do
+ it 'creates an audit log record' do
expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
- expect(SecurityEvent.last.details[:with]).to eq("standard")
+ expect(SecurityEvent.last.details[:with]).to eq('standard')
+ end
+
+ include_examples 'user login request with unique ip limit', 302 do
+ def request
+ post(:create, user: { login: user.username, password: user.password })
+ expect(subject.current_user).to eq user
+ subject.sign_out user
+ end
end
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index c9584ddf18c..f67d26da0ac 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,4 +1,9 @@
require 'spec_helper'
+shared_examples 'content not cached without revalidation' do
+ it 'ensures content will not be cached without revalidation' do
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate')
+ end
+end
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
@@ -50,6 +55,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -59,6 +71,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -76,6 +95,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -88,6 +114,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -133,6 +166,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -157,6 +197,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -169,6 +216,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -205,6 +259,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -234,6 +295,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -246,6 +314,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -291,6 +366,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
end
diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb
new file mode 100644
index 00000000000..82f44fa3d15
--- /dev/null
+++ b/spec/factories/chat_teams.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :chat_team, class: ChatTeam do
+ sequence :team_id do |n|
+ "abcdefghijklm#{n}"
+ end
+
+ namespace factory: :group
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index a90534d10ba..6b0d084614b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -15,8 +15,8 @@ FactoryGirl.define do
options do
{
- image: "ruby:2.1",
- services: ["postgres"]
+ image: 'ruby:2.1',
+ services: ['postgres']
}
end
@@ -57,7 +57,7 @@ FactoryGirl.define do
end
trait :manual do
- status 'skipped'
+ status 'manual'
self.when 'manual'
end
@@ -71,11 +71,26 @@ FactoryGirl.define do
allow_failure true
end
+ trait :ignored do
+ allowed_to_fail
+ end
+
trait :playable do
- skipped
manual
end
+ trait :tags do
+ tag_list [:docker, :ruby]
+ end
+
+ trait :on_tag do
+ tag true
+ end
+
+ trait :triggered do
+ trigger_request factory: :ci_trigger_request_with_variables
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
@@ -151,5 +166,31 @@ FactoryGirl.define do
allow(build).to receive(:commit).and_return build(:commit)
end
end
+
+ trait :extended_options do
+ options do
+ {
+ image: 'ruby:2.1',
+ services: ['postgres'],
+ after_script: "ls\ndate",
+ artifacts: {
+ name: 'artifacts_file',
+ untracked: false,
+ paths: ['out/'],
+ when: 'always',
+ expire_in: '7d'
+ },
+ cache: {
+ key: 'cache_key',
+ untracked: false,
+ paths: ['vendor/*']
+ }
+ }
+ end
+ end
+
+ trait :no_options do
+ options { {} }
+ end
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 77404f46c92..b67c96bc00d 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -40,6 +40,14 @@ FactoryGirl.define do
trait :invalid do
config(rspec: nil)
end
+
+ trait :blocked do
+ status :manual
+ end
+
+ trait :success do
+ status :success
+ end
end
end
end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3372e5ab685..6712dd5d82e 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
- gl_project_id 1
+ project_id 1
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 756b341ecba..169590deb8e 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -35,6 +35,10 @@ FactoryGirl.define do
status 'created'
end
+ trait :manual do
+ status 'manual'
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 22f84150bb3..21487541507 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
author
association :source_project, :repository, factory: :project
target_project { source_project }
+ project { target_project }
# $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
@@ -59,8 +60,8 @@ FactoryGirl.define do
target_branch "master"
end
- trait :merge_when_build_succeeds do
- merge_when_build_succeeds true
+ trait :merge_when_pipeline_succeeds do
+ merge_when_pipeline_succeeds true
merge_user author
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 5c50cd7f4ad..fe19a404e16 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -26,12 +26,17 @@ FactoryGirl.define do
factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
association :project, :repository
+
+ transient do
+ line_number 14
+ end
+
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
- new_line: 14,
+ new_line: line_number,
diff_refs: noteable.diff_refs
)
end
diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb
new file mode 100644
index 00000000000..543b3e99274
--- /dev/null
+++ b/spec/factories/oauth_access_grants.rb
@@ -0,0 +1,11 @@
+FactoryGirl.define do
+ factory :oauth_access_grant do
+ resource_owner_id { create(:user).id }
+ application
+ token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+ expires_in 2.hours
+
+ redirect_uri { application.redirect_uri }
+ scopes { application.scopes }
+ end
+end
diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb
index ccf02d0719b..a46bc1d8ce8 100644
--- a/spec/factories/oauth_access_tokens.rb
+++ b/spec/factories/oauth_access_tokens.rb
@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :oauth_access_token do
resource_owner
application
- token '123456'
+ token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+ scopes { application.scopes }
end
end
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index d116a573830..86cdc208268 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -1,7 +1,7 @@
FactoryGirl.define do
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
name { FFaker::Name.name }
- uid { FFaker::Name.name }
+ uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
redirect_uri { FFaker::Internet.uri('http') }
owner
owner_type 'User'
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 811eab7e15b..7b15ba47de1 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -6,5 +6,22 @@ FactoryGirl.define do
revoked false
expires_at { 5.days.from_now }
scopes ['api']
+ impersonation false
+
+ trait :impersonation do
+ impersonation true
+ end
+
+ trait :revoked do
+ revoked true
+ end
+
+ trait :expired do
+ expires_at { 1.day.ago }
+ end
+
+ trait :invalid do
+ token nil
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index c80b09e9b9d..0db2fe04edd 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -38,13 +38,17 @@ FactoryGirl.define do
trait :empty_repo do
after(:create) do |project|
- project.create_repository
+ raise "Failed to create repository!" unless project.create_repository
+
+ # We delete hooks so that gitlab-shell will not try to authenticate with
+ # an API that isn't running
+ FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'hooks'))
end
end
trait :broken_repo do
after(:create) do |project|
- project.create_repository
+ raise "Failed to create repository!" unless project.create_repository
FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
end
@@ -138,27 +142,24 @@ FactoryGirl.define do
project.add_user(args[:user], args[:access])
- project.repository.commit_file(
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/bug.md",
'something valid',
message: 'test 3',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/template_test.md",
'template_test',
message: 'test 1',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/feature_proposal.md",
'feature_proposal',
message: 'test 2',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
end
end
end
@@ -188,27 +189,19 @@ FactoryGirl.define do
factory :jira_project, parent: :project do
has_external_issue_tracker true
-
- after :create do |project|
- project.create_jira_service(
- active: true,
- properties: {
- title: 'JIRA tracker',
- url: 'http://jira.example.net',
- project_key: 'JIRA'
- }
- )
- end
+ jira_service
end
factory :kubernetes_project, parent: :empty_project do
+ kubernetes_service
+ end
+
+ factory :prometheus_project, parent: :empty_project do
after :create do |project|
- project.create_kubernetes_service(
+ project.create_prometheus_service(
active: true,
properties: {
- namespace: project.path,
- api_url: 'https://kubernetes.example.com',
- token: 'a' * 40,
+ api_url: 'https://prometheus.example.com'
}
)
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index a14a46c803e..88f6c265505 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -2,4 +2,23 @@ FactoryGirl.define do
factory :service do
project factory: :empty_project
end
+
+ factory :kubernetes_service do
+ project factory: :empty_project
+ active true
+ properties({
+ namespace: 'somepath',
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40,
+ })
+ end
+
+ factory :jira_service do
+ project factory: :empty_project
+ active true
+ properties(
+ url: 'https://jira.example.com',
+ project_key: 'jira-key'
+ )
+ end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index a5265f1b189..c1ac3bb84ad 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -18,11 +18,6 @@ FactoryGirl.define do
action { Todo::DIRECTLY_ADDRESSED }
end
- trait :on_commit do
- commit_id RepoHelpers.sample_commit.id
- target_type "Commit"
- end
-
trait :build_failed do
action { Todo::BUILD_FAILED }
target factory: :merge_request
@@ -48,4 +43,13 @@ FactoryGirl.define do
state :done
end
end
+
+ factory :on_commit_todo, class: Todo do
+ project factory: :empty_project
+ author
+ user
+ action { Todo::ASSIGNED }
+ commit_id RepoHelpers.sample_commit.id
+ target_type "Commit"
+ end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 1732b1a0081..249dabbaae8 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -26,6 +26,11 @@ FactoryGirl.define do
two_factor_via_otp
end
+ trait :ghost do
+ ghost true
+ after(:build) { |user, _| user.block! }
+ end
+
trait :two_factor_via_otp do
before(:create) do |user|
user.otp_required_for_login = true
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index e8e080ce3e2..273cacd82cd 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
scenario 'shows only HTTP url' do
visit_project
- expect(page).to have_content("git clone #{project.http_url_to_repo}")
+ expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}")
expect(page).not_to have_selector('#clone-dropdown')
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index de42ab81fac..03daab12c8f 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do
visit admin_application_settings_path
end
+ scenario 'Change visibility settings' do
+ choose "application_setting_default_project_visibility_20"
+ click_button 'Save'
+
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
scenario 'Change application settings' do
uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
@@ -26,7 +33,7 @@ feature 'Admin updates settings', feature: true do
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
- page.check('Notify only broken builds')
+ page.check('Notify only broken pipelines')
check_all_events
click_on 'Save'
@@ -50,7 +57,6 @@ feature 'Admin updates settings', feature: true do
page.check('Note')
page.check('Issue')
page.check('Merge request')
- page.check('Build')
page.check('Pipeline')
end
end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
new file mode 100644
index 00000000000..9ff5c2f9d40
--- /dev/null
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do
+ let(:admin) { create(:admin) }
+ let!(:user) { create(:user) }
+
+ def active_impersonation_tokens
+ find(".table.active-tokens")
+ end
+
+ def inactive_impersonation_tokens
+ find(".table.inactive-tokens")
+ end
+
+ before { login_as(admin) }
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ name = FFaker::Product.brand
+
+ visit admin_user_impersonation_tokens_path(user_id: user.username)
+ fill_in "Name", with: name
+
+ # Set date to 1st of next month
+ find_field("Expires at").trigger('focus')
+ find(".pika-next").click
+ click_on "1"
+
+ # Scopes
+ check "api"
+ check "read_user"
+
+ expect { click_on "Create Impersonation Token" }.to change { PersonalAccessTokensFinder.new(impersonation: true).execute.count }
+ expect(active_impersonation_tokens).to have_text(name)
+ expect(active_impersonation_tokens).to have_text('In')
+ expect(active_impersonation_tokens).to have_text('api')
+ expect(active_impersonation_tokens).to have_text('read_user')
+ end
+ end
+
+ describe 'active tokens' do
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'only shows impersonation tokens' do
+ visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+ expect(active_impersonation_tokens).to have_text(impersonation_token.name)
+ expect(active_impersonation_tokens).not_to have_text(personal_access_token.name)
+ end
+ end
+
+ describe "inactive tokens" do
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ it "allows revocation of an active impersonation token" do
+ visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+ click_on "Revoke"
+
+ expect(inactive_impersonation_tokens).to have_text(impersonation_token.name)
+ end
+
+ it "moves expired tokens to the 'inactive' section" do
+ impersonation_token.update(expires_at: 5.days.ago)
+
+ visit admin_user_impersonation_tokens_path(user_id: user.username)
+
+ expect(inactive_impersonation_tokens).to have_text(impersonation_token.name)
+ end
+ end
+end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 21ee6cedbae..58b14e09740 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe "Dashboard Issues Feed", feature: true do
describe "GET /issues" do
- let!(:user) { create(:user) }
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
@@ -23,7 +24,7 @@ describe "Dashboard Issues Feed", feature: true do
visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
@@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do
end
context "issue with basic fields" do
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+ let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
it "renders issue fields" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true do
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
expect(entry).to be_present
- expect(entry).to have_selector('author email', text: issue2.author_email)
- expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).to have_selector('author email', text: issue2.author_public_email)
+ expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
expect(entry).not_to have_selector('labels')
expect(entry).not_to have_selector('milestone')
expect(entry).to have_selector('description', text: issue2.description)
@@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do
context "issue with label and milestone" do
let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
let!(:label1) { create(:label, project: project1, title: 'label1') }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+ let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
before do
issue1.labels << label1
@@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true do
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
expect(entry).to be_present
- expect(entry).to have_selector('author email', text: issue1.author_email)
- expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('author email', text: issue1.author_public_email)
+ expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
expect(entry).to have_selector('labels label', text: label1.title)
expect(entry).to have_selector('milestone', text: milestone1.title)
expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 863412d18eb..b3903ec2faf 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe 'Issues Feed', feature: true do
describe 'GET /issues' do
- let!(:user) { create(:user) }
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
- let!(:issue) { create(:issue, author: user, project: project) }
+ let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) }
before do
project.team << [user, :developer]
@@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true do
expect(response_headers['Content-Type']).
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
- expect(body).to have_selector('author email', text: issue.author_email)
+ expect(body).to have_selector('author email', text: issue.author_public_email)
+ expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
@@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true do
expect(response_headers['Content-Type']).
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
- expect(body).to have_selector('author email', text: issue.author_email)
+ expect(body).to have_selector('author email', text: issue.author_public_email)
+ expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
@@ -43,7 +46,7 @@ describe 'Issues Feed', feature: true do
:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
@@ -54,7 +57,7 @@ describe 'Issues Feed', feature: true do
visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index f8c3ccb416b..55e10a1a89b 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -57,11 +57,11 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in notes' do
- expect(body).to match /Bug confirmed <img[^>]*\/>/
+ expect(body).to match /Bug confirmed <gl-emoji[^>]*>/
end
it 'has XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
+ expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
end
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 2875fc1e533..d17a418b8c3 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -49,6 +49,12 @@ describe 'Issue Boards add issue modal', :feature, :js do
expect(page).not_to have_selector('.add-issues-modal')
end
+
+ it 'does not show tooltip on add issues button' do
+ button = page.find('.filter-dropdown-container button', text: 'Add issues')
+
+ expect(button[:title]).not_to eq("Please add a list to your board first")
+ end
end
context 'issues list' do
@@ -101,6 +107,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys(issue.title)
+ find('.form-control').native.send_keys(:enter)
+
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
@@ -109,6 +118,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns no issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing search')
+ find('.form-control').native.send_keys(:enter)
+
+ wait_for_vue_resource
expect(page).not_to have_selector('.card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 1b25b51cfb2..f7e8b78b54d 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -28,10 +28,10 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('Welcome to your Issue Board!')
end
- it 'disables add issues button by default' do
- button = page.find('.issue-boards-search button', text: 'Add issues')
+ it 'shows tooltip on add issues button' do
+ button = page.find('.filter-dropdown-container button', text: 'Add issues')
- expect(button[:disabled]).to eq true
+ expect(button[:"data-original-title"]).to eq("Please add a list to your board first")
end
it 'hides the blank state when clicking nevermind button' do
@@ -71,16 +71,16 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
- let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
- let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
- let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
- let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
- let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
- let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
- let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
+ let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
+ let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
+ let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
+ let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
+ let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit namespace_project_board_path(project.namespace, project, board)
@@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'search done list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue8.title)
- end
+ find('.filtered-search').set(issue8.title)
+ find('.filtered-search').native.send_keys(:enter)
wait_for_vue_resource
@@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'search list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue5.title)
- end
+ find('.filtered-search').set(issue5.title)
+ find('.filtered-search').native.send_keys(:enter)
wait_for_vue_resource
@@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(find('.issue-boards-search')).to have_selector('.open')
+ expect(page).to have_css('#js-add-list.open')
end
it 'creates new list from a new label' do
@@ -359,17 +357,9 @@ describe 'Issue Boards', feature: true, js: true do
context 'filtering' do
it 'filters by author' do
- page.within '.issues-filters' do
- click_button('Author')
- wait_for_ajax
-
- page.within '.dropdown-menu-author' do
- click_link(user2.name)
- end
- wait_for_vue_resource
-
- expect(find('.js-author-search')).to have_content(user2.name)
- end
+ set_filter("author", user2.username)
+ click_filter_link(user2.username)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -377,17 +367,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by assignee' do
- page.within '.issues-filters' do
- click_button('Assignee')
- wait_for_ajax
-
- page.within '.dropdown-menu-assignee' do
- click_link(user.name)
- end
- wait_for_vue_resource
-
- expect(find('.js-assignee-search')).to have_content(user.name)
- end
+ set_filter("assignee", user.username)
+ click_filter_link(user.username)
+ submit_filter
wait_for_vue_resource
@@ -396,17 +378,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by milestone' do
- page.within '.issues-filters' do
- click_button('Milestone')
- wait_for_ajax
-
- page.within '.milestone-filter' do
- click_link(milestone.title)
- end
- wait_for_vue_resource
-
- expect(find('.js-milestone-select')).to have_content(milestone.title)
- end
+ set_filter("milestone", "\"#{milestone.title}\"")
+ click_filter_link(milestone.title)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -415,16 +389,9 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by label' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
wait_for_vue_resource
wait_for_board_cards(1, 1)
@@ -432,19 +399,14 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by label with space after reload' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(accepting.title)
- wait_for_vue_resource(spinner: false)
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", "\"#{accepting.title}\"")
+ click_filter_link(accepting.title)
+ submit_filter
# Test after reload
page.evaluate_script 'window.location.reload()'
+ wait_for_board_cards(1, 1)
+ wait_for_empty_boards((2..3))
wait_for_vue_resource
@@ -460,26 +422,16 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'removes filtered labels' do
- wait_for_vue_resource
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
- page.within '.labels-filter' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource(spinner: false)
- end
-
- expect(page).to have_css('input[name="label_name[]"]', visible: false)
+ wait_for_board_cards(1, 1)
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource(spinner: false)
- end
+ find('.clear-search').click
+ submit_filter
- expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
- end
+ wait_for_board_cards(1, 8)
end
it 'infinite scrolls list with label filter' do
@@ -487,16 +439,9 @@ describe 'Issue Boards', feature: true, js: true do
create(:labeled_issue, project: project, labels: [planning, testing])
end
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link(testing.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
+ submit_filter
wait_for_vue_resource
@@ -518,18 +463,13 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'filters by multiple labels' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
+ set_filter("label", testing.title)
+ click_filter_link(testing.title)
- page.within(find('.dropdown-menu-labels')) do
- click_link(testing.title)
- wait_for_vue_resource
- click_link(bug.title)
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
+ set_filter("label", bug.title)
+ click_filter_link(bug.title)
+
+ submit_filter
wait_for_vue_resource
@@ -545,14 +485,14 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ page.within('.tokens-container') do
+ expect(page).to have_content(bug.title)
+ end
+
wait_for_vue_resource
wait_for_board_cards(1, 1)
wait_for_empty_boards((2..3))
-
- page.within('.labels-filter') do
- expect(find('.dropdown-toggle-text')).to have_content(bug.title)
- end
end
it 'removes label filter by clicking label button on issue' do
@@ -560,16 +500,13 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.card', match: :first)) do
click_button(bug.title)
end
+
wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
wait_for_vue_resource
-
- page.within('.labels-filter') do
- expect(find('.dropdown-toggle-text')).to have_content(bug.title)
- end
end
end
end
@@ -643,4 +580,20 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_board_cards(board, 0)
end
end
+
+ def set_filter(type, text)
+ find('.filtered-search').native.send_keys("#{type}:#{text}")
+ end
+
+ def submit_filter
+ find('.filtered-search').native.send_keys(:enter)
+ end
+
+ def click_filter_link(link_text)
+ page.within('.filtered-search-input-container') do
+ expect(page).to have_button(link_text)
+
+ click_button(link_text)
+ end
+ end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
new file mode 100644
index 00000000000..c50155a6d14
--- /dev/null
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -0,0 +1,166 @@
+require 'rails_helper'
+
+describe 'Issue Boards', :feature, :js do
+ include WaitForVueResource
+ include DragTo
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project) }
+ let!(:list1) { create(:list, board: board, label: label, position: 0) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
+ context 'un-ordered issues' do
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'has un-ordered issue as last issue' do
+ page.within(first('.board')) do
+ expect(all('.card').last).to have_content(issue4.title)
+ end
+ end
+
+ it 'moves un-ordered issue to top of list' do
+ drag(from_index: 3, to_index: 0)
+
+ page.within(first('.board')) do
+ expect(first('.card')).to have_content(issue4.title)
+ end
+ end
+ end
+
+ context 'ordering in list' do
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'moves from middle to top' do
+ drag(from_index: 1, to_index: 0)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue2.title)
+ end
+
+ it 'moves from middle to bottom' do
+ drag(from_index: 1, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue2.title)
+ end
+
+ it 'moves from top to bottom' do
+ drag(from_index: 0, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue3.title)
+ end
+
+ it 'moves from bottom to top' do
+ drag(from_index: 2, to_index: 0)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue1.title)
+ end
+
+ it 'moves from top to middle' do
+ drag(from_index: 0, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue2.title)
+ end
+
+ it 'moves from bottom to middle' do
+ drag(from_index: 2, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue2.title)
+ end
+ end
+
+ context 'ordering when changing list' do
+ let(:label2) { create(:label, project: project) }
+ let!(:list2) { create(:list, board: board, label: label2, position: 1) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) }
+ let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'moves to top of another list' do
+ drag(list_from_index: 0, list_to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(first('.card')).to have_content(issue3.title)
+ end
+ end
+
+ it 'moves to bottom of another list' do
+ drag(list_from_index: 0, list_to_index: 1, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(all('.card').last).to have_content(issue3.title)
+ end
+ end
+
+ it 'moves to index of another list' do
+ drag(list_from_index: 0, list_to_index: 1, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(all('.card')[1]).to have_content(issue3.title)
+ end
+ end
+ end
+
+ def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index)
+ end
+end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 1cf0d11d448..e2281a7da55 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards add issue modal filtering', :feature, :js do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing empty state')
+ find('.form-control').native.send_keys(:enter)
wait_for_vue_resource
@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
it 'restores filters when closing' do
visit_board
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Upcoming'
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
expect(page).to have_selector('.card', count: 0)
@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end
end
- context 'author' do
- let!(:issue) { create(:issue, project: project, author: user2) }
-
- before do
- project.team << [user2, :developer]
+ it 'resotres filters after clicking clear button' do
+ visit_board
- visit_board
- end
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
- it 'filters by any author' do
- page.within('.add-issues-modal') do
- click_button 'Author'
+ page.within('.add-issues-modal') do
+ wait_for_vue_resource
- wait_for_ajax
+ expect(page).to have_selector('.card', count: 0)
- click_link 'Any Author'
+ find('.clear-search').click
- wait_for_vue_resource
+ wait_for_vue_resource
- expect(page).to have_selector('.card', count: 2)
- end
+ expect(page).to have_selector('.card', count: 1)
end
+ end
- it 'filters by selected user' do
- page.within('.add-issues-modal') do
- click_button 'Author'
+ context 'author' do
+ let!(:issue) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user2, :developer]
- wait_for_ajax
+ visit_board
+ end
- click_link user2.name
+ it 'filters by selected user' do
+ set_filter('author')
+ click_filter_link(user2.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any assignee' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- click_link 'Any Assignee'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by unassigned' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- click_link 'Unassigned'
+ set_filter('assignee')
+ click_filter_link('No Assignee')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1)
end
end
it 'filters by selected user' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- page.within '.dropdown-menu-user' do
- click_link user2.name
- end
+ set_filter('assignee')
+ click_filter_link(user2.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Any Milestone'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by upcoming milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Upcoming'
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'upcoming')
expect(page).to have_selector('.card', count: 0)
end
end
it 'filters by selected milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link milestone.name
+ set_filter('milestone')
+ click_filter_link(milestone.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: milestone.name)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link 'Any Label'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by no label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link 'No Label'
+ set_filter('label')
+ click_filter_link('No Label')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1)
end
end
it 'filters by label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link label.title
+ set_filter('label')
+ click_filter_link(label.title)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: label.title)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
click_button('Add issues')
end
+
+ def set_filter(type, text = '')
+ find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}")
+ end
+
+ def submit_filter
+ find('.add-issues-modal .filtered-search').native.send_keys(:enter)
+ end
+
+ def click_filter_link(link_text)
+ page.within('.add-issues-modal .filtered-search-input-container') do
+ expect(page).to have_button(link_text)
+
+ click_button(link_text)
+ end
+ end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 59e87b3f69c..3332e07ec31 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -11,8 +11,8 @@ describe 'Issue Boards', feature: true, js: true do
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
- let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { first('.board').first('.card') }
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 324ede798fe..0e305c52358 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -192,7 +192,7 @@ describe 'Commits' do
commits = project.repository.commits(branch_name)
commits.each do |commit|
- expect(page).to have_content("committed #{commit.committed_date}")
+ expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
end
end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index fec86128d03..55df7e45f79 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -2,433 +2,594 @@ require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper
+ include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
before do
- @feat = MarkdownFeature.new
+ login_as :admin
+ end
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ describe 'Copying rendered GFM' do
+ before do
+ @feat = MarkdownFeature.new
- visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
- end
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
- # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
- # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
- # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
+ visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ end
- # These are all in a single `it` for performance reasons.
- it 'works', :aggregate_failures do
- verify(
- 'nesting',
+ # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM.
+ # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+ # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
- '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
- )
+ # These are all in a single `it` for performance reasons.
+ it 'works', :aggregate_failures do
+ verify(
+ 'nesting',
- verify(
- 'a real world example from the gitlab-ce README',
+ '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ )
- <<-GFM.strip_heredoc
- # GitLab
+ verify(
+ 'a real world example from the gitlab-ce README',
- [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
- [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+ <<-GFM.strip_heredoc
+ # GitLab
- ## Canonical source
+ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+ [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+ [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+ [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
- The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+ ## Canonical source
- ## Open source software to collaborate on code
+ The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
- To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+ ## Open source software to collaborate on code
+ To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
- - Manage Git repositories with fine grained access controls that keep your code secure
- - Perform code reviews and enhance collaboration with merge requests
+ - Manage Git repositories with fine grained access controls that keep your code secure
- - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+ - Perform code reviews and enhance collaboration with merge requests
- - Each project can also have an issue tracker, issue board, and a wiki
+ - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+ - Each project can also have an issue tracker, issue board, and a wiki
- - Completely free and open source (MIT Expat license)
- GFM
- )
+ - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- verify(
- 'InlineDiffFilter',
+ - Completely free and open source (MIT Expat license)
+ GFM
+ )
- '{-Deleted text-}',
- '{+Added text+}'
- )
+ verify(
+ 'InlineDiffFilter',
- verify(
- 'TaskListFilter',
+ '{-Deleted text-}',
+ '{+Added text+}'
+ )
- '- [ ] Unchecked task',
- '- [x] Checked task',
- '1. [ ] Unchecked numbered task',
- '1. [x] Checked numbered task'
- )
+ verify(
+ 'TaskListFilter',
- verify(
- 'ReferenceFilter',
+ '- [ ] Unchecked task',
+ '- [x] Checked task',
+ '1. [ ] Unchecked numbered task',
+ '1. [x] Checked numbered task'
+ )
- # issue reference
- @feat.issue.to_reference,
- # full issue reference
- @feat.issue.to_reference(full: true),
- # issue URL
- namespace_project_issue_url(@project.namespace, @project, @feat.issue),
- # issue URL with note anchor
- namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
- # issue link
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
- # issue link with note anchor
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
- )
+ verify(
+ 'ReferenceFilter',
- verify(
- 'AutolinkFilter',
+ # issue reference
+ @feat.issue.to_reference,
+ # full issue reference
+ @feat.issue.to_reference(full: true),
+ # issue URL
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue),
+ # issue URL with note anchor
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
+ # issue link
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
+ # issue link with note anchor
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ )
- 'https://example.com'
- )
+ verify(
+ 'AutolinkFilter',
- verify(
- 'TableOfContentsFilter',
+ 'https://example.com'
+ )
- '[[_TOC_]]'
- )
+ verify(
+ 'TableOfContentsFilter',
- verify(
- 'EmojiFilter',
+ '[[_TOC_]]'
+ )
- ':thumbsup:'
- )
+ verify(
+ 'EmojiFilter',
- verify(
- 'ImageLinkFilter',
-
- '![Image](https://example.com/image.png)'
- )
+ ':thumbsup:'
+ )
- verify(
- 'VideoLinkFilter',
+ verify(
+ 'ImageLinkFilter',
+
+ '![Image](https://example.com/image.png)'
+ )
- '![Video](https://example.com/video.mp4)'
- )
+ verify(
+ 'VideoLinkFilter',
- verify(
- 'MathFilter: math as converted from GFM to HTML',
+ '![Video](https://example.com/video.mp4)'
+ )
- '$`c = \pm\sqrt{a^2 + b^2}`$',
+ verify(
+ 'MathFilter: math as converted from GFM to HTML',
- # math block
- <<-GFM.strip_heredoc
- ```math
- c = \pm\sqrt{a^2 + b^2}
- ```
- GFM
- )
+ '$`c = \pm\sqrt{a^2 + b^2}`$',
- aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
- gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+ # math block
+ <<-GFM.strip_heredoc
+ ```math
+ c = \pm\sqrt{a^2 + b^2}
+ ```
+ GFM
+ )
- html = <<-HTML.strip_heredoc
- <span class="katex">
- <span class="katex-mathml">
- <math>
- <semantics>
- <mrow>
- <mi>c</mi>
- <mo>=</mo>
- <mo>±</mo>
- <msqrt>
- <mrow>
- <msup>
- <mi>a</mi>
- <mn>2</mn>
- </msup>
- <mo>+</mo>
- <msup>
- <mi>b</mi>
- <mn>2</mn>
- </msup>
- </mrow>
- </msqrt>
- </mrow>
- <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
- </semantics>
- </math>
- </span>
- <span class="katex-html" aria-hidden="true">
- <span class="strut" style="height: 0.913389em;"></span>
- <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
- <span class="base textstyle uncramped">
- <span class="mord mathit">c</span>
- <span class="mrel">=</span>
- <span class="mord">±</span>
- <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
- <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
- </span>
- <span class="vlist">
- <span class="" style="top: 0em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- <span class="mord textstyle cramped">
- <span class="mord">
- <span class="mord mathit">a</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+ gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+
+ html = <<-HTML.strip_heredoc
+ <span class="katex">
+ <span class="katex-mathml">
+ <math>
+ <semantics>
+ <mrow>
+ <mi>c</mi>
+ <mo>=</mo>
+ <mo>±</mo>
+ <msqrt>
+ <mrow>
+ <msup>
+ <mi>a</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <msup>
+ <mi>b</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </msqrt>
+ </mrow>
+ <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
+ </semantics>
+ </math>
+ </span>
+ <span class="katex-html" aria-hidden="true">
+ <span class="strut" style="height: 0.913389em;"></span>
+ <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
+ <span class="base textstyle uncramped">
+ <span class="mord mathit">c</span>
+ <span class="mrel">=</span>
+ <span class="mord">±</span>
+ <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
+ <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
+ </span>
+ <span class="vlist">
+ <span class="" style="top: 0em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="mord textstyle cramped">
+ <span class="mord">
+ <span class="mord mathit">a</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
- </span>
- <span class="mbin">+</span>
- <span class="mord">
- <span class="mord mathit">b</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ <span class="mbin">+</span>
+ <span class="mord">
+ <span class="mord mathit">b</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- <span class="" style="top: -0.833389em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
+ <span class="" style="top: -0.833389em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
</span>
- <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- HTML
+ HTML
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
- verify(
- 'SanitizationFilter',
+ verify(
+ 'SanitizationFilter',
- <<-GFM.strip_heredoc
- <a name="named-anchor"></a>
-
- <sub>sub</sub>
+ <<-GFM.strip_heredoc
+ <a name="named-anchor"></a>
- <dl>
- <dt>dt</dt>
- <dd>dd</dd>
- </dl>
+ <sub>sub</sub>
- <kbd>kbd</kbd>
+ <dl>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ </dl>
- <q>q</q>
+ <kbd>kbd</kbd>
- <samp>samp</samp>
+ <q>q</q>
- <var>var</var>
+ <samp>samp</samp>
- <ruby>ruby</ruby>
+ <var>var</var>
- <rt>rt</rt>
+ <ruby>ruby</ruby>
- <rp>rp</rp>
+ <rt>rt</rt>
- <abbr>abbr</abbr>
- GFM
- )
+ <rp>rp</rp>
- verify(
- 'SanitizationFilter',
+ <abbr>abbr</abbr>
- <<-GFM.strip_heredoc,
- ```
- Plain text
- ```
- GFM
+ <summary>summary</summary>
- <<-GFM.strip_heredoc,
- ```ruby
- def foo
- bar
- end
- ```
- GFM
+ <details>details</details>
+ GFM
+ )
+
+ verify(
+ 'SanitizationFilter',
+
+ <<-GFM.strip_heredoc,
+ ```
+ Plain text
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ def foo
+ bar
+ end
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc
+ Foo
+
+ This is an example of GFM
+
+ ```js
+ Code goes here
+ ```
+ GFM
+ )
+
+ verify(
+ 'MarkdownFilter',
+
+ "Line with two spaces at the end \nto insert a linebreak",
+
+ '`code`',
+ '`` code with ` ticks ``',
+
+ '> Quote',
+
+ # multiline quote
+ <<-GFM.strip_heredoc,
+ > Multiline
+ > Quote
+ >
+ > With multiple paragraphs
+ GFM
+
+ '![Image](https://example.com/image.png)',
+
+ '# Heading with no anchor link',
+
+ '[Link](https://example.com)',
+
+ '- List item',
+
+ # multiline list item
+ <<-GFM.strip_heredoc,
+ - Multiline
+ List item
+ GFM
+
+ # nested lists
+ <<-GFM.strip_heredoc,
+ - Nested
+
+
+ - Lists
+ GFM
+
+ # list with blockquote
+ <<-GFM.strip_heredoc,
+ - List
+
+ > Blockquote
+ GFM
+
+ '1. Numbered list item',
+
+ # multiline numbered list item
+ <<-GFM.strip_heredoc,
+ 1. Multiline
+ Numbered list item
+ GFM
+
+ # nested numbered list
+ <<-GFM.strip_heredoc,
+ 1. Nested
- <<-GFM.strip_heredoc
- Foo
- This is an example of GFM
+ 1. Numbered lists
+ GFM
- ```js
- Code goes here
- ```
- GFM
- )
+ '# Heading',
+ '## Heading',
+ '### Heading',
+ '#### Heading',
+ '##### Heading',
+ '###### Heading',
- verify(
- 'MarkdownFilter',
+ '**Bold**',
- "Line with two spaces at the end \nto insert a linebreak",
+ '_Italics_',
- '`code`',
- '`` code with ` ticks ``',
+ '~~Strikethrough~~',
- '> Quote',
+ '2^2',
- # multiline quote
- <<-GFM.strip_heredoc,
- > Multiline
- > Quote
- >
- > With multiple paragraphs
- GFM
+ '-----',
- '![Image](https://example.com/image.png)',
+ # table
+ <<-GFM.strip_heredoc,
+ | Centered | Right | Left |
+ |:--------:|------:|------|
+ | Foo | Bar | **Baz** |
+ | Foo | Bar | **Baz** |
+ GFM
- '# Heading with no anchor link',
+ # table with empty heading
+ <<-GFM.strip_heredoc,
+ | | x | y |
+ |---|---|---|
+ | a | 1 | 0 |
+ | b | 0 | 1 |
+ GFM
+ )
+ end
- '[Link](https://example.com)',
+ alias_method :gfm_to_html, :markdown
- '- List item',
+ def verify(label, *gfms)
+ aggregate_failures(label) do
+ gfms.each do |gfm|
+ html = gfm_to_html(gfm)
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+ end
+ end
- # multiline list item
- <<-GFM.strip_heredoc,
- - Multiline
- List item
- GFM
+ # Fake a `current_user` helper
+ def current_user
+ @feat.user
+ end
+ end
- # nested lists
- <<-GFM.strip_heredoc,
- - Nested
+ describe 'Copying code' do
+ let(:project) { create(:project) }
+ context 'from a diff' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
- - Lists
- GFM
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
- # list with blockquote
- <<-GFM.strip_heredoc,
- - List
+ '`RuntimeError`'
+ )
+ end
+ end
- > Blockquote
- GFM
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
- '1. Numbered list item',
+ '`raise RuntimeError, "System commands must be given as an array of strings"`'
+ )
+ end
+ end
- # multiline numbered list item
- <<-GFM.strip_heredoc,
- 1. Multiline
- Numbered list item
- GFM
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+ )
+ end
+ end
+ end
- # nested numbered list
- <<-GFM.strip_heredoc,
- 1. Nested
+ context 'from a blob' do
+ before do
+ visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ end
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"] .no',
- 1. Numbered lists
- GFM
+ '`RuntimeError`'
+ )
+ end
+ end
- '# Heading',
- '## Heading',
- '### Heading',
- '#### Heading',
- '##### Heading',
- '###### Heading',
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"]',
- '**Bold**',
+ '`raise RuntimeError, "System commands must be given as an array of strings"`'
+ )
+ end
+ end
- '_Italics_',
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block' do
+ verify(
+ '.line[id="LC9"], .line[id="LC10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+ )
+ end
+ end
+ end
- '~~Strikethrough~~',
+ context 'from a GFM code block' do
+ before do
+ visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ end
- '2^2',
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"] .s2',
- '-----',
+ '`"bio"`'
+ )
+ end
+ end
- # table
- <<-GFM.strip_heredoc,
- | Centered | Right | Left |
- |:--------:|------:|------|
- | Foo | Bar | **Baz** |
- | Foo | Bar | **Baz** |
- GFM
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"]',
- # table with empty heading
- <<-GFM.strip_heredoc,
- | | x | y |
- |---|---|---|
- | a | 1 | 0 |
- | b | 0 | 1 |
- GFM
- )
+ '`"bio": null,`'
+ )
+ end
+ end
+
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block with the correct language' do
+ verify(
+ '.line[id="LC27"], .line[id="LC28"]',
+
+ <<-GFM.strip_heredoc,
+ ```json
+ "bio": null,
+ "skype": "",
+ ```
+ GFM
+ )
+ end
+ end
+ end
+
+ def verify(selector, gfm)
+ html = html_for_selector(selector)
+ output_gfm = html_to_gfm(html, 'transformCodeSelection')
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
end
- alias_method :gfm_to_html, :markdown
+ def html_for_selector(selector)
+ js = <<-JS.strip_heredoc
+ (function(selector) {
+ var els = document.querySelectorAll(selector);
+ var htmls = _.map(els, function(el) { return el.outerHTML; });
+ return htmls.join("\\n");
+ })("#{escape_javascript(selector)}")
+ JS
+ page.evaluate_script(js)
+ end
- def html_to_gfm(html)
+ def html_to_gfm(html, transformer = 'transformGFMSelection')
js = <<-JS.strip_heredoc
(function(html) {
+ var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+
var node = document.createElement('div');
node.innerHTML = html;
+
+ node = transformer(node);
+ if (!node) return null;
+
return window.gl.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
end
-
- def verify(label, *gfms)
- aggregate_failures(label) do
- gfms.each do |gfm|
- html = gfm_to_html(gfm)
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
- end
- end
-
- # Fake a `current_user` helper
- def current_user
- @feat.user
- end
end
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
new file mode 100644
index 00000000000..c977f266296
--- /dev/null
+++ b/spec/features/dashboard/activity_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Activity', feature: true do
+ before do
+ login_as(create :user)
+ visit activity_dashboard_path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index 038c1641be9..f33bcbb5318 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -25,4 +25,19 @@ RSpec.describe 'Dashboard Archived Project', feature: true do
expect(page).to have_link(project.name)
expect(page).to have_link(archived_project.name)
end
+
+ it 'searchs archived projects', :js do
+ click_button 'Last updated'
+ click_link 'Show archived projects'
+
+ expect(page).to have_link(project.name)
+ expect(page).to have_link(archived_project.name)
+
+ fill_in 'project-filter-form-field', with: archived_project.name
+
+ find('#project-filter-form-field').native.send_keys :return
+
+ expect(page).not_to have_link(project.name)
+ expect(page).to have_link(archived_project.name)
+ end
end
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
new file mode 100644
index 00000000000..ca04107d33a
--- /dev/null
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'Dashboard Groups page', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:user) { create :user }
+ let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, :nested) }
+ let!(:another_group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ nested_group.add_owner(user)
+
+ login_as(user)
+
+ visit dashboard_groups_path
+ end
+
+ it 'shows groups user is member of' do
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_content(nested_group.full_name)
+ expect(page).not_to have_content(another_group.full_name)
+ end
+
+ it 'filters groups' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_ajax
+
+ expect(page).to have_content(group.full_name)
+ expect(page).not_to have_content(nested_group.full_name)
+ expect(page).not_to have_content(another_group.full_name)
+ end
+
+ it 'resets search when user cleans the input' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_ajax
+
+ fill_in 'filter_groups', with: ""
+ wait_for_ajax
+
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_content(nested_group.full_name)
+ expect(page).not_to have_content(another_group.full_name)
+ expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+ end
+end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 2db1cf71209..f4420814c3a 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -45,4 +45,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
expect(page).to have_content(assigned_issue.title)
expect(page).to have_content(other_issue.title)
end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index ba77093a6d4..49d93db58a9 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -12,7 +12,7 @@ feature 'Project member activity', feature: true, js: true do
def visit_activities_and_wait_with_event(event_type)
Event.create(project: project, author_id: user.id, action: event_type)
- visit activity_namespace_project_path(project.namespace.path, project.path)
+ visit activity_namespace_project_path(project.namespace, project)
wait_for_ajax
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
new file mode 100644
index 00000000000..c4e58d14f75
--- /dev/null
+++ b/spec/features/dashboard/projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Projects', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: "awesome stuff") }
+
+ before do
+ project.team << [user, :developer]
+ login_as user
+ visit dashboard_projects_path
+ end
+
+ it 'shows the project the user in a member of in the list' do
+ visit dashboard_projects_path
+ expect(page).to have_content('awesome stuff')
+ end
+
+ describe "with a pipeline" do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+ before do
+ pipeline
+ end
+
+ it 'shows that the last pipeline passed' do
+ visit dashboard_projects_path
+ expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
+ end
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+end
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index c2e0612aef8..34d6257f5fd 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -1,26 +1,45 @@
require 'spec_helper'
-describe "Dashboard > User filters projects", feature: true do
+describe 'Dashboard > User filters projects', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+ let(:user2) { create(:user) }
+ let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
describe 'filtering personal projects' do
before do
- user = create(:user)
- project = create(:project, name: "Victorialand", namespace: user.namespace)
- project.team << [user, :master]
-
- user2 = create(:user)
- project2 = create(:project, name: "Treasure", namespace: user2.namespace)
project2.team << [user, :developer]
- login_as(user)
visit dashboard_projects_path
end
it 'filters by projects "Owned by me"' do
- click_link "Owned by me"
+ click_link 'Owned by me'
expect(page).to have_css('.is-active', text: 'Owned by me')
expect(page).to have_content('Victorialand')
expect(page).not_to have_content('Treasure')
end
end
+
+ describe 'filtering starred projects', :js do
+ before do
+ user.toggle_star(project)
+
+ visit dashboard_projects_path
+ end
+
+ it 'returns message when starred projects fitler returns no results' do
+ fill_in 'project-filter-form-field', with: 'Beta\n'
+
+ expect(page).to have_content('No projects found')
+ expect(page).not_to have_content('You don\'t have starred projects yet')
+ end
+ end
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index b898f9bc64f..8c61cdebc4b 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -48,10 +48,10 @@ describe "Dashboard Issues filtering", feature: true, js: true do
it 'updates atom feed link' do
visit_issues(milestone_title: '', assignee_id: user.id)
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
+ link = find('.nav-controls a[title="Subscribe"]')
+ params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('milestone_title' => [''])
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
deleted file mode 100644
index c203e1f20c1..00000000000
--- a/spec/features/environment_spec.rb
+++ /dev/null
@@ -1,225 +0,0 @@
-require 'spec_helper'
-
-feature 'Environment', :feature do
- given(:project) { create(:empty_project) }
- given(:user) { create(:user) }
- given(:role) { :developer }
-
- background do
- login_as(user)
- project.team << [user, role]
- end
-
- feature 'environment details page' do
- given!(:environment) { create(:environment, project: project) }
- given!(:deployment) { }
- given!(:manual) { }
-
- before do
- visit_environment(environment)
- end
-
- scenario 'shows environment name' do
- expect(page).to have_content(environment.name)
- end
-
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
- end
- end
-
- context 'with deployments' do
- context 'when there is no related deployable' do
- given(:deployment) do
- create(:deployment, environment: environment, deployable: nil)
- end
-
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
-
- scenario 'does not show a re-deploy button for deployment without build' do
- expect(page).not_to have_link('Re-deploy')
- end
-
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
- end
-
- context 'with related deployable present' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
-
- given(:deployment) do
- create(:deployment, environment: environment, deployable: build)
- end
-
- scenario 'does show build name' do
- expect(page).to have_link("#{build.name} (##{build.id})")
- end
-
- scenario 'does show re-deploy button' do
- expect(page).to have_link('Re-deploy')
- end
-
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
-
- context 'with manual action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
-
- scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
- end
-
- scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
- end
-
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
- end
-
- context 'with terminal' do
- let(:project) { create(:kubernetes_project, :test_repo) }
-
- context 'for project master' do
- let(:role) { :master }
-
- scenario 'it shows the terminal button' do
- expect(page).to have_terminal_button
- end
-
- context 'web terminal', :js do
- before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
- visit terminal_namespace_project_environment_path(project.namespace, project, environment)
- end
-
- it 'displays a web terminal' do
- expect(page).to have_selector('#terminal')
- end
-
- it 'displays a link to the environment external url' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
- end
- end
-
- context 'for developer' do
- let(:role) { :developer }
-
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
- end
- end
-
- context 'when environment is available' do
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
-
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
-
- scenario 'does allow to stop environment' do
- click_link('Stop')
-
- expect(page).to have_content('close_app')
- end
-
- context 'for reporter' do
- let(:role) { :reporter }
-
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
- end
- end
-
- context 'without stop action' do
- scenario 'does allow to stop environment' do
- click_link('Stop')
- end
- end
- end
-
- context 'when environment is stopped' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
-
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
- end
- end
- end
- end
- end
-
- feature 'auto-close environment when branch is deleted' do
- given(:project) { create(:project) }
-
- given!(:environment) do
- create(:environment, :with_review_app, project: project,
- ref: 'feature')
- end
-
- scenario 'user visits environment page' do
- visit_environment(environment)
-
- expect(page).to have_link('Stop')
- end
-
- scenario 'user deletes the branch with running environment' do
- visit namespace_project_branches_path(project.namespace, project)
-
- remove_branch_with_hooks(project, user, 'feature') do
- page.within('.js-branch-feature') { find('a.btn-remove').click }
- end
-
- visit_environment(environment)
-
- expect(page).to have_no_link('Stop')
- end
-
- ##
- # This is a workaround for problem described in #24543
- #
- def remove_branch_with_hooks(project, user, branch)
- params = {
- oldrev: project.commit(branch).id,
- newrev: Gitlab::Git::BLANK_SHA,
- ref: "refs/heads/#{branch}"
- }
-
- yield
-
- GitPushService.new(project, user, params).execute
- end
- end
-
- def visit_environment(environment)
- visit namespace_project_environment_path(environment.project.namespace,
- environment.project,
- environment)
- end
-
- def have_terminal_button
- have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
- end
-end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
deleted file mode 100644
index 78be7d36f47..00000000000
--- a/spec/features/environments_spec.rb
+++ /dev/null
@@ -1,245 +0,0 @@
-require 'spec_helper'
-
-feature 'Environments page', :feature, :js do
- given(:project) { create(:empty_project) }
- given(:user) { create(:user) }
- given(:role) { :developer }
-
- background do
- project.team << [user, role]
- login_as(user)
- end
-
- given!(:environment) { }
- given!(:deployment) { }
- given!(:manual) { }
-
- before do
- visit_environments(project)
- end
-
- describe 'page tabs' do
- scenario 'shows "Available" and "Stopped" tab with links' do
- expect(page).to have_link('Available')
- expect(page).to have_link('Stopped')
- end
- end
-
- context 'without environments' do
- scenario 'does show no environments' do
- expect(page).to have_content('You don\'t have any environments right now.')
- end
-
- scenario 'does show 0 as counter for environments in both tabs' do
- expect(page.find('.js-available-environments-count').text).to eq('0')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
- end
-
- describe 'when showing the environment' do
- given(:environment) { create(:environment, project: project) }
-
- scenario 'does show environment name' do
- expect(page).to have_link(environment.name)
- end
-
- scenario 'does show number of available and stopped environments' do
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
-
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('No deployments yet')
- end
-
- context 'for available environment' do
- given(:environment) { create(:environment, project: project, state: :available) }
-
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
- end
-
- context 'for stopped environment' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
-
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
- end
- end
-
- context 'with deployments' do
- given(:project) { create(:project) }
-
- given(:deployment) do
- create(:deployment, environment: environment,
- sha: project.commit.id)
- end
-
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
-
- scenario 'does show deployment internal id' do
- expect(page).to have_content(deployment.iid)
- end
-
- context 'with build and manual actions' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
-
- given(:manual) do
- create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
- end
-
- given(:deployment) do
- create(:deployment, environment: environment,
- deployable: build,
- sha: project.commit.id)
- end
-
- scenario 'does show a play button' do
- find('.js-dropdown-play-icon-container').click
- expect(page).to have_content(manual.name.humanize)
- end
-
- scenario 'does allow to play manual action', js: true do
- expect(manual).to be_skipped
-
- find('.js-dropdown-play-icon-container').click
- expect(page).to have_content(manual.name.humanize)
-
- expect { click_link(manual.name.humanize) }
- .not_to change { Ci::Pipeline.count }
-
- expect(manual.reload).to be_pending
- end
-
- scenario 'does show build name and id' do
- expect(page).to have_link("#{build.name} ##{build.id}")
- end
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
-
- scenario 'does not show external link button' do
- expect(page).not_to have_css('external-url')
- end
-
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
-
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
- end
-
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
-
- scenario 'does show stop button' do
- expect(page).to have_selector('.stop-env-link')
- end
-
- scenario 'starts build when stop button clicked' do
- find('.stop-env-link').click
-
- expect(page).to have_content('close_app')
- end
-
- context 'for reporter' do
- let(:role) { :reporter }
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
- end
- end
-
- context 'with terminal' do
- let(:project) { create(:kubernetes_project, :test_repo) }
-
- context 'for project master' do
- let(:role) { :master }
-
- scenario 'it shows the terminal button' do
- expect(page).to have_terminal_button
- end
- end
-
- context 'for developer' do
- let(:role) { :developer }
-
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
- end
- end
- end
- end
- end
-
- scenario 'does have a New environment button' do
- expect(page).to have_link('New environment')
- end
-
- describe 'when creating a new environment' do
- before do
- visit_environments(project)
- end
-
- context 'when logged as developer' do
- before do
- click_link 'New environment'
- end
-
- context 'for valid name' do
- before do
- fill_in('Name', with: 'production')
- click_on 'Save'
- end
-
- scenario 'does create a new pipeline' do
- expect(page).to have_content('production')
- end
- end
-
- context 'for invalid name' do
- before do
- fill_in('Name', with: 'name,with,commas')
- click_on 'Save'
- end
-
- scenario 'does show errors' do
- expect(page).to have_content('Name can contain only letters')
- end
- end
- end
-
- context 'when logged as reporter' do
- given(:role) { :reporter }
-
- scenario 'does not have a New environment link' do
- expect(page).not_to have_link('New environment')
- end
- end
- end
-
- def have_terminal_button
- have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
- end
-
- def visit_environments(project)
- visit namespace_project_environments_path(project.namespace, project)
- end
-end
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
new file mode 100644
index 00000000000..773ae4b38bc
--- /dev/null
+++ b/spec/features/explore/groups_list_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'Explore Groups page', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:user) { create :user }
+ let!(:group) { create(:group) }
+ let!(:public_group) { create(:group, :public) }
+ let!(:private_group) { create(:group, :private) }
+
+ before do
+ group.add_owner(user)
+
+ login_as(user)
+
+ visit explore_groups_path
+ end
+
+ it 'shows groups user is member of' do
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_content(public_group.full_name)
+ expect(page).not_to have_content(private_group.full_name)
+ end
+
+ it 'filters groups' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_ajax
+
+ expect(page).to have_content(group.full_name)
+ expect(page).not_to have_content(public_group.full_name)
+ expect(page).not_to have_content(private_group.full_name)
+ end
+
+ it 'resets search when user cleans the input' do
+ fill_in 'filter_groups', with: group.name
+ wait_for_ajax
+
+ fill_in 'filter_groups', with: ""
+ wait_for_ajax
+
+ expect(page).to have_content(group.full_name)
+ expect(page).to have_content(public_group.full_name)
+ expect(page).not_to have_content(private_group.full_name)
+ expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
+ end
+end
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
new file mode 100644
index 00000000000..3b481cba424
--- /dev/null
+++ b/spec/features/groups/activity_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+feature 'Group activity page', feature: true do
+ let(:group) { create(:group) }
+ let(:path) { activity_group_path(group) }
+
+ context 'when signed in' do
+ before do
+ user = create(:group_member, :developer, user: create(:user), group: group ).user
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
new file mode 100644
index 00000000000..8528718a2f7
--- /dev/null
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Group name toggle', feature: true, js: true do
+ let(:group) { create(:group) }
+ let(:nested_group_1) { create(:group, parent: group) }
+ let(:nested_group_2) { create(:group, parent: nested_group_1) }
+ let(:nested_group_3) { create(:group, parent: nested_group_2) }
+
+ before do
+ login_as :user
+ end
+
+ it 'is not present for less than 3 groups' do
+ visit group_path(group)
+ expect(page).not_to have_css('.group-name-toggle')
+
+ visit group_path(nested_group_1)
+ expect(page).not_to have_css('.group-name-toggle')
+ end
+
+ it 'is present for nested group of 3 or more in the namespace' do
+ visit group_path(nested_group_2)
+ expect(page).to have_css('.group-name-toggle')
+
+ visit group_path(nested_group_3)
+ expect(page).to have_css('.group-name-toggle')
+ end
+
+ context 'for group with at least 3 groups' do
+ before do
+ visit group_path(nested_group_2)
+ end
+
+ it 'should show the full group namespace when toggled' do
+ expect(page).not_to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: false)
+
+ click_button '...'
+
+ expect(page).to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: true)
+ end
+ end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 476eca17a9d..1b3747c390b 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -5,4 +5,22 @@ feature 'Group issues page', feature: true do
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', Issue
+
+ context 'rss feed' do
+ let(:access_level) { ProjectFeature::ENABLED }
+
+ context 'when signed in' do
+ let(:user) { user_in_group }
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ let(:user) { nil }
+
+ it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+ end
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
new file mode 100644
index 00000000000..fb39693e8ca
--- /dev/null
+++ b/spec/features/groups/show_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Group show page', feature: true do
+ let(:group) { create(:group) }
+ let(:path) { group_path(group) }
+
+ context 'when signed in' do
+ before do
+ user = create(:group_member, :developer, user: create(:user), group: group ).user
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 37b7c20239f..d243f9478bb 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -43,6 +43,44 @@ feature 'Group', feature: true do
expect(page).to have_namespace_error_message
end
end
+
+ describe 'Mattermost team creation' do
+ before do
+ allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled)
+
+ visit new_group_path
+ end
+
+ context 'Mattermost enabled' do
+ let(:mattermost_enabled) { true }
+
+ it 'displays a team creation checkbox' do
+ expect(page).to have_selector('#group_create_chat_team')
+ end
+
+ it 'checks the checkbox by default' do
+ expect(find('#group_create_chat_team')['checked']).to eq(true)
+ end
+
+ it 'updates the team URL on graph path update', :js do
+ out_span = find('span[data-bind-out="create_chat_team"]')
+
+ expect(out_span.text).to be_empty
+
+ fill_in('group_path', with: 'test-group')
+
+ expect(out_span.text).to eq('test-group')
+ end
+ end
+
+ context 'Mattermost disabled' do
+ let(:mattermost_enabled) { false }
+
+ it 'doesnt show a team creation checkbox if Mattermost not enabled' do
+ expect(page).not_to have_selector('#group_create_chat_team')
+ end
+ end
+ end
end
describe 'create a nested group' do
@@ -105,7 +143,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.group-home-desc > p > img')
+ expect(page).to have_css('.group-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 40a1fced8d8..e0b2404e60a 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -33,4 +33,30 @@ describe 'Help Pages', feature: true do
it_behaves_like 'help page', prefix: '/gitlab'
end
end
+
+ context 'in a production environment with version check enabled', js: true do
+ before do
+ allow(Rails.env).to receive(:production?) { true }
+ allow(current_application_settings).to receive(:version_check_enabled) { true }
+ allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' }
+
+ login_as :user
+ visit help_path
+ end
+
+ it 'should display a version check image' do
+ expect(find('.js-version-status-badge')).to be_visible
+ end
+
+ it 'should have a src url' do
+ expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/)
+ end
+
+ it 'should hide the version check image if the image request fails' do
+ # We use '--load-images=no' with poltergeist so we must trigger manually
+ execute_script("$('.js-version-status-badge').trigger('error');")
+
+ expect(find('.js-version-status-badge', visible: false)).not_to be_visible
+ end
+ end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 73553f97d6f..bfe43bff10f 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do
end
def selected_sort_order
- find('.pull-right .dropdown button').text.downcase
+ find('.filter-dropdown-container .dropdown button').text.downcase
end
def visit_merge_requests_with_state(project, state)
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index e31bc40adc3..b90bf6268fd 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -30,6 +30,13 @@ describe 'issuable list', feature: true do
end
end
+ it "counts merge requests closing issues icons for each issue" do
+ visit_issuable_list(:issue)
+
+ expect(page).to have_selector('.icon-merge-request-unmerged', count: 1)
+ expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1)
+ end
+
def visit_issuable_list(issuable_type)
if issuable_type == :issue
visit namespace_project_issues_path(project.namespace, project)
@@ -40,11 +47,12 @@ describe 'issuable list', feature: true do
def create_issuables(issuable_type)
3.times do
- if issuable_type == :issue
- issuable = create(:issue, project: project, author: user)
- else
- issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
- end
+ issuable =
+ if issuable_type == :issue
+ create(:issue, project: project, author: user)
+ else
+ create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
2.times do
create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
@@ -53,5 +61,15 @@ describe 'issuable list', feature: true do
create(:award_emoji, :downvote, awardable: issuable)
create(:award_emoji, :upvote, awardable: issuable)
end
+
+ if issuable_type == :issue
+ issue = Issue.reorder(:iid).first
+ merge_request = create(:merge_request,
+ title: FFaker::Lorem.sentence,
+ source_project: project,
+ source_branch: FFaker::Name.name)
+
+ MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+ end
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 73e43316dc7..16e453bc328 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -17,22 +17,35 @@ describe 'Awards Emoji', feature: true do
login_as(user)
end
+ describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
+ before do
+ # The `heart_tip` emoji is not valid anymore so we need to skip validation
+ issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
+ it 'does not shows a 500 page' do
+ expect(page).to have_text(issue.title)
+ end
+ end
+
describe 'Click award emoji from issue#show' do
- let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'increments the thumbsdown emoji', js: true do
- find('[data-emoji="thumbsdown"]').click
+ find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do
- find('[data-emoji="thumbsup"]').click
+ find('[data-name="thumbsup"]').click
wait_for_ajax
expect(thumbsup_emoji).to have_text("1")
end
@@ -44,7 +57,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
- find('[data-emoji="thumbsdown"]').click
+ find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
@@ -67,6 +80,18 @@ describe 'Awards Emoji', feature: true do
expect(page).not_to have_selector(emoji_counter)
end
end
+
+ context 'execute /award slash command' do
+ it 'toggles the emoji award on noteable', js: true do
+ execute_slash_command('/award :100:')
+
+ expect(find(noteable_award_counter)).to have_text("1")
+
+ execute_slash_command('/award :100:')
+
+ expect(page).not_to have_selector(noteable_award_counter)
+ end
+ end
end
end
@@ -80,6 +105,15 @@ describe 'Awards Emoji', feature: true do
end
end
+ def execute_slash_command(cmd)
+ within('.js-main-target-form') do
+ fill_in 'note[note]', with: cmd
+ click_button 'Comment'
+ end
+
+ wait_for_ajax
+ end
+
def thumbsup_emoji
page.all(emoji_counter).first
end
@@ -92,15 +126,19 @@ describe 'Awards Emoji', feature: true do
'span.js-counter'
end
+ def noteable_award_counter
+ ".awards .active"
+ end
+
def toggle_smiley_emoji(status)
within('.note') do
find('.note-emoji-button').click
end
unless status
- first('[data-emoji="smiley"]').click
+ first('[data-name="smiley"]').click
else
- find('[data-emoji="smiley"]').click
+ find('[data-name="smiley"]').click
end
wait_for_ajax
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 832757b24d4..2f59630b4fb 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -55,7 +55,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'to all issues' do
before do
check 'check_all_issues'
- open_labels_dropdown ['bug', 'feature']
+ open_labels_dropdown %w(bug feature)
update_issues
end
@@ -70,7 +70,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'to a issue' do
before do
check "selected_issue_#{issue1.id}"
- open_labels_dropdown ['bug', 'feature']
+ open_labels_dropdown %w(bug feature)
update_issues
end
@@ -112,7 +112,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
- unmark_labels_in_dropdown ['bug', 'feature']
+ unmark_labels_in_dropdown %w(bug feature)
update_issues
end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 762cab0c0e1..572bca3de21 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,76 +1,93 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', feature: true do
+feature 'Resolving all open discussions in a merge request from an issue', feature: true, js: true do
let(:user) { create(:user) }
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
- before do
- project.team << [user, :master]
- login_as user
- end
-
- context 'with the internal tracker disabled' do
+ describe 'as a user with access to the project' do
before do
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ project.team << [user, :master]
+ login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'does not show a link to create a new issue' do
- expect(page).not_to have_link 'open an issue to resolve them later'
- end
- end
-
- context 'merge request has discussions that need to be resolved' do
- before do
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ it 'shows a button to resolve all discussions by creating a new issue' do
+ within('li#resolve-count-app') do
+ expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
end
- it 'shows a warning that the merge request contains unresolved discussions' do
- expect(page).to have_content 'This merge request has unresolved discussions'
- end
+ context 'resolving the discussion' do
+ before do
+ click_button 'Resolve discussion'
+ end
- it 'has a link to resolve all discussions by creating an issue' do
- page.within '.mr-widget-body' do
- expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ it 'hides the link for creating a new issue' do
+ expect(page).not_to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
context 'creating an issue for discussions' do
before do
- page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid)
+ click_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
- it 'shows an issue with the title filled in' do
- title_field = page.find_field('issue[title]')
+ it_behaves_like 'creating an issue for a discussion'
+ end
- expect(title_field.value).to include(merge_request.title)
+ context 'for a project where all discussions need to be resolved before merging' do
+ before do
+ project.update_attribute(:only_allow_merge_if_all_discussions_are_resolved, true)
end
- it 'has a mention of the discussion in the description' do
- description_field = page.find_field('issue[description]')
+ context 'with the internal tracker disabled' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
- expect(description_field.value).to include(discussion.first_note.note)
+ it 'does not show a link to create a new issue' do
+ expect(page).not_to have_link 'open an issue to resolve them later'
+ end
end
- it 'has a hidden field for the merge request' do
- merge_request_field = find('#merge_request_for_resolving_discussions', visible: false)
-
- expect(merge_request_field.value).to eq(merge_request.iid.to_s)
- end
+ context 'merge request has discussions that need to be resolved' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
- it 'can create a new issue for the project' do
- expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
- end
+ it 'shows a warning that the merge request contains unresolved discussions' do
+ expect(page).to have_content 'This merge request has unresolved discussions'
+ end
- it 'resolves the discussion in the merge request' do
- click_button 'Submit issue'
+ it 'has a link to resolve all discussions by creating an issue' do
+ page.within '.mr-widget-body' do
+ expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+ end
- discussion.first_note.reload
+ context 'creating an issue for discussions' do
+ before do
+ page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
- expect(discussion.resolved?).to eq(true)
+ it_behaves_like 'creating an issue for a discussion'
+ end
end
end
end
+
+ describe 'as a reporter' do
+ before do
+ project.team << [user, :reporter]
+ login_as user
+ visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+
+ it 'Shows a notice to ask someone else to resolve the discussions' do
+ expect(page).to have_content("The discussions at #{merge_request.to_reference} will stay unresolved. Ask someone with permission to resolve them.")
+ end
+ end
end
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
new file mode 100644
index 00000000000..88e2cc60d79
--- /dev/null
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+feature 'Resolve an open discussion in a merge request by creating an issue', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first }
+
+ describe 'As a user with access to the project' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'with the internal tracker disabled' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to create a new issue' do
+ expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ end
+ end
+
+ context 'resolving the discussion', js: true do
+ before do
+ click_button 'Resolve discussion'
+ end
+
+ it 'hides the link for creating a new issue' do
+ expect(page).not_to have_link 'Resolve this discussion in a new issue'
+ end
+
+ it 'shows the link for creating a new issue when unresolving a discussion' do
+ page.within '.diff-content' do
+ click_button 'Unresolve discussion'
+ end
+
+ expect(page).to have_link 'Resolve this discussion in a new issue'
+ end
+ end
+
+ it 'has a link to create a new issue for a discussion' do
+ new_issue_link = new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+
+ expect(page).to have_link 'Resolve this discussion in a new issue', href: new_issue_link
+ end
+
+ context 'creating the issue' do
+ before do
+ click_link 'Resolve this discussion in a new issue', href: new_namespace_project_issue_path(project.namespace, project, discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid)
+ end
+
+ it 'has a hidden field for the discussion' do
+ discussion_field = find('#discussion_to_resolve', visible: false)
+
+ expect(discussion_field.value).to eq(discussion.id.to_s)
+ end
+
+ it_behaves_like 'creating an issue for a discussion'
+ end
+ end
+
+ describe 'as a reporter' do
+ before do
+ project.team << [user, :reporter]
+ login_as user
+ visit new_namespace_project_issue_path(project.namespace, project,
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id)
+ end
+
+ it 'Shows a notice to ask someone else to resolve the discussions' do
+ expect(page).to have_content("The discussion at #{merge_request.to_reference}"\
+ "(discussion #{discussion.first_note.id}) will stay unresolved."\
+ "Ask someone with permission to resolve it.")
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 93763f092fb..4dcc56a97d1 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
-describe 'Dropdown assignee', js: true, feature: true do
+describe 'Dropdown assignee', :feature, :js do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -9,17 +10,10 @@ describe 'Dropdown assignee', js: true, feature: true do
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
-
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 5
- wait_for_ajax
- end
- end
+ let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
def dropdown_assignee_size
- page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
+ filter_dropdown.all('.filter-dropdown-item').size
end
def click_assignee(text)
@@ -56,63 +50,80 @@ describe 'Dropdown assignee', js: true, feature: true do
end
it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
- expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
+ expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
it 'should load all the assignees when opened' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
expect(dropdown_assignee_size).to eq(3)
end
it 'shows current user at top of dropdown' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
- expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name)
+ expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end
end
describe 'filtering' do
before do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
+
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
end
it 'filters by name' do
- send_keys_to_filtered_search('j')
+ filtered_search.send_keys('j')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by case insensitive name' do
- send_keys_to_filtered_search('J')
+ filtered_search.send_keys('J')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by username with symbol' do
- send_keys_to_filtered_search('@ot')
+ filtered_search.send_keys('@ot')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username with symbol' do
- send_keys_to_filtered_search('@OT')
+ filtered_search.send_keys('@OT')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by username without symbol' do
- send_keys_to_filtered_search('ot')
+ filtered_search.send_keys('ot')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username without symbol' do
- send_keys_to_filtered_search('OT')
+ filtered_search.send_keys('OT')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
end
@@ -125,22 +136,25 @@ describe 'Dropdown assignee', js: true, feature: true do
click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
+ expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the assignee username when the assignee has been filtered' do
- send_keys_to_filtered_search('roo')
+ filtered_search.send_keys('roo')
click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user.username} ")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:none ")
+ expect_tokens([{ name: 'assignee', value: 'none' }])
+ expect_filtered_search_input_empty
end
end
@@ -173,7 +187,7 @@ describe 'Dropdown assignee', js: true, feature: true do
describe 'caching requests' do
it 'caches requests after the first load' do
filtered_search.set('assignee')
- send_keys_to_filtered_search(':')
+ filtered_search.send_keys(':')
initial_size = dropdown_assignee_size
expect(initial_size).to be > 0
@@ -182,7 +196,7 @@ describe 'Dropdown assignee', js: true, feature: true do
project.team << [new_user, :master]
find('.filtered-search-input-container .clear-search').click
filtered_search.set('assignee')
- send_keys_to_filtered_search(':')
+ filtered_search.send_keys(':')
expect(dropdown_assignee_size).to eq(initial_size)
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 59e302f0e2d..1772a120045 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -13,9 +14,10 @@ describe 'Dropdown author', js: true, feature: true do
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
- sleep 5
- wait_for_ajax
end
+
+ sleep 0.5
+ wait_for_ajax
end
def dropdown_author_size
@@ -121,14 +123,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
+ expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user.username} ")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 04dd54ab459..01b657bcada 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' do
@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect(filtered_search.value).to eq('assignee:')
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect(filtered_search.value).to eq('milestone:')
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect(filtered_search.value).to eq('label:')
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
end
end
@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' do
@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect(filtered_search.value).to eq('assignee:')
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect(filtered_search.value).to eq('milestone:')
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect(filtered_search.value).to eq('label:')
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ describe 'reselecting from dropdown' do
+ it 'reuses existing author text' do
+ filtered_search.send_keys('author:')
+ filtered_search.send_keys(:backspace)
+ click_hint('author')
+
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing assignee text' do
+ filtered_search.send_keys('assignee:')
+ filtered_search.send_keys(:backspace)
+ click_hint('assignee')
+
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing milestone text' do
+ filtered_search.send_keys('milestone:')
+ filtered_search.send_keys(:backspace)
+ click_hint('milestone')
+
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing label text' do
+ filtered_search.send_keys('label:')
+ filtered_search.send_keys(:backspace)
+ click_hint('label')
+
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index ab3b868fd3a..b192064b693 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
end
@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end
it 'filters by case-insensitive name with or without symbol' do
- search_for_label('b')
+ filtered_search.send_keys('b')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field
init_label_search
- search_for_label('~bu')
+ filtered_search.send_keys('~bu')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name when the label is partially filled' do
@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
+ expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
+ expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
+ expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{special_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{special_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no label`' do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:none ")
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 0ce16715b86..ce96a420699 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-describe 'Dropdown milestone', js: true, feature: true do
- include WaitForAjax
+describe 'Dropdown milestone', :feature, :js do
+ include FilteredSearchHelpers
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
@@ -14,18 +14,10 @@ describe 'Dropdown milestone', js: true, feature: true do
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
-
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 3
- wait_for_ajax
- sleep 3
- end
- end
+ let(:filter_dropdown) { find("#{js_dropdown_milestone} .filter-dropdown") }
def dropdown_milestone_size
- page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
+ filter_dropdown.all('.filter-dropdown-item').size
end
def click_milestone(text)
@@ -64,13 +56,14 @@ describe 'Dropdown milestone', js: true, feature: true do
end
it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('milestone:')
+ filtered_search.set('milestone:')
- expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
+ expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
it 'should load all the milestones when opened' do
- send_keys_to_filtered_search('milestone:')
+ filtered_search.set('milestone:')
expect(dropdown_milestone_size).to be > 0
end
@@ -78,41 +71,48 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'filtering' do
before do
- filtered_search.set('milestone')
+ filtered_search.set('milestone:')
+
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
end
it 'filters by name' do
- send_keys_to_filtered_search(':v1')
+ filtered_search.send_keys('v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name' do
- send_keys_to_filtered_search(':V1')
+ filtered_search.send_keys('V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by name with symbol' do
- send_keys_to_filtered_search(':%v1')
+ filtered_search.send_keys('%v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name with symbol' do
- send_keys_to_filtered_search(':%V1')
+ filtered_search.send_keys('%V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters' do
- send_keys_to_filtered_search(':(+')
+ filtered_search.send_keys('(+')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters with symbol' do
- send_keys_to_filtered_search(':%(+')
+ filtered_search.send_keys('%(+')
expect(dropdown_milestone_size).to eq(1)
end
@@ -121,70 +121,94 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'selecting from dropdown' do
before do
filtered_search.set('milestone:')
+
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
end
it 'fills in the milestone name when the milestone has not been filled' do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name when the milestone is partially filled' do
- send_keys_to_filtered_search('v')
+ filtered_search.send_keys('v')
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
+ expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
+ expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
+ expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:none ")
+ expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_filtered_search_input_empty
end
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:upcoming ")
+ expect_tokens([{ name: 'milestone', value: 'upcoming' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'selects `started milestones`' do
+ click_static_milestone('Started')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_filtered_search_input_empty
end
end
@@ -222,16 +246,14 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('milestone')
- send_keys_to_filtered_search(':')
+ filtered_search.set('milestone:')
initial_size = dropdown_milestone_size
expect(initial_size).to be > 0
create(:milestone, project: project)
find('.filtered-search-input-container .clear-search').click
- filtered_search.set('milestone')
- send_keys_to_filtered_search(':')
+ filtered_search.set('milestone:')
expect(dropdown_milestone_size).to eq(initial_size)
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 0420e64d42c..f463312bf57 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,4 +1,4 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
- let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
- let!(:milestone) { create(:milestone, title: "8", project: project) }
+ let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
@@ -97,7 +96,9 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched author' do
input_filtered_search("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(5)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid author' do
@@ -110,36 +111,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'author with other filters' do
+ let(:search_term) { 'issue' }
+
it 'filters issues by searched author and text' do
- search = "author:@#{user.username} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} #{search_term}")
+ expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(3)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee and text' do
- search = "author:@#{user.username} assignee:@#{user.username} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(3)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee, label, and text' do
- search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee, label, milestone and text' do
- search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -151,19 +166,19 @@ describe 'Filter issues', js: true, feature: true do
describe 'filter issues by assignee' do
context 'only assignee' do
it 'filters issues by searched assignee' do
- search = "assignee:@#{user.username}"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(5)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by no assignee' do
- search = "assignee:none"
- input_filtered_search(search)
+ input_filtered_search('assignee:none')
+ expect_tokens([{ name: 'assignee', value: 'none' }])
expect_issues_list_count(8, 1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid assignee' do
@@ -176,36 +191,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'assignee with other filters' do
+ let(:search_term) { 'searchTerm' }
+
it 'filters issues by searched assignee and text' do
- search = "assignee:@#{user.username} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} #{search_term}")
+ expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author and text' do
- search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author, label, text' do
- search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author, label, milestone and text' do
- search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -217,21 +246,23 @@ describe 'Filter issues', js: true, feature: true do
end
describe 'filter issues by label' do
+ let(:search_term) { 'bug' }
+
context 'only label' do
it 'filters issues by searched label' do
- search = "label:~#{bug_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title}")
+ expect_tokens([{ name: 'label', value: bug_label.title }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by no label' do
- search = "label:none"
- input_filtered_search(search)
+ input_filtered_search('label:none')
+ expect_tokens([{ name: 'label', value: 'none' }])
expect_issues_list_count(9, 1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid label' do
@@ -239,11 +270,14 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by multiple labels' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by label containing special characters' do
@@ -251,21 +285,20 @@ describe 'Filter issues', js: true, feature: true do
special_issue = create(:issue, title: "Issue with special character label", project: project)
special_issue.labels << special_label
- search = "label:~#{special_label.title}"
- input_filtered_search(search)
-
+ input_filtered_search("label:~#{special_label.title}")
+ expect_tokens([{ name: 'label', value: special_label.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'does not show issues' do
- new_label = create(:label, project: project, title: "new_label")
+ new_label = create(:label, project: project, title: 'new_label')
- search = "label:~#{new_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{new_label.title}")
+ expect_tokens([{ name: 'label', value: new_label.title }])
expect_no_issues_list()
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
@@ -275,29 +308,29 @@ describe 'Filter issues', js: true, feature: true do
special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
special_multiple_issue.labels << special_multiple_label
- search = "label:~'#{special_multiple_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{special_multiple_label.title}'")
+ # filtered search defaults quotations to double quotes
+ expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
expect_issues_list_count(1)
- # filtered search defaults quotations to double quotes
- expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
+ expect_filtered_search_input_empty
end
it 'single quotes' do
- search = "label:~'#{multiple_words_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{multiple_words_label.title}'")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
+ expect_filtered_search_input_empty
end
it 'double quotes' do
- search = "label:~\"#{multiple_words_label.title}\""
- input_filtered_search(search)
+ input_filtered_search("label:~\"#{multiple_words_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'single quotes containing double quotes' do
@@ -305,11 +338,11 @@ describe 'Filter issues', js: true, feature: true do
double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
double_quotes_label_issue.labels << double_quotes_label
- search = "label:~'#{double_quotes_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{double_quotes_label.title}'")
+ expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'double quotes containing single quotes' do
@@ -317,86 +350,115 @@ describe 'Filter issues', js: true, feature: true do
single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
single_quotes_label_issue.labels << single_quotes_label
- search = "label:~\"#{single_quotes_label.title}\""
- input_filtered_search(search)
+ input_filtered_search("label:~\"#{single_quotes_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
context 'label with other filters' do
it 'filters issues by searched label and text' do
- search = "label:~#{caps_sensitive_label.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author, assignee and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author, assignee, milestone and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
context 'multiple labels with other filters' do
it 'filters issues by searched label, label2, and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author, assignee and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author, assignee, milestone and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
context 'issue label clicked' do
before do
find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
- sleep 1
end
it 'filters' do
@@ -404,7 +466,8 @@ describe 'Filter issues', js: true, feature: true do
end
it 'displays in search bar' do
- expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
+ expect_filtered_search_input_empty
end
end
@@ -420,19 +483,33 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched milestone' do
input_filtered_search("milestone:%#{milestone.title}")
+ expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(5)
+ expect_filtered_search_input_empty
end
it 'filters issues by no milestone' do
input_filtered_search("milestone:none")
+ expect_tokens([{ name: 'milestone', value: 'none' }])
expect_issues_list_count(7, 1)
+ expect_filtered_search_input_empty
end
it 'filters issues by upcoming milestones' do
input_filtered_search("milestone:upcoming")
+ expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_issues_list_count(1)
+ expect_filtered_search_input_empty
+ end
+
+ it 'filters issues by started milestones' do
+ input_filtered_search("milestone:started")
+
+ expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_issues_list_count(5)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid milestones' do
@@ -447,55 +524,69 @@ describe 'Filter issues', js: true, feature: true do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
- search = "milestone:%#{special_milestone.title}"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{special_milestone.title}")
+ expect_tokens([{ name: 'milestone', value: special_milestone.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'does not show issues' do
new_milestone = create(:milestone, title: "new", project: project)
- search = "milestone:%#{new_milestone.title}"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{new_milestone.title}")
+ expect_tokens([{ name: 'milestone', value: new_milestone.title }])
expect_no_issues_list()
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
context 'milestone with other filters' do
+ let(:search_term) { 'bug' }
+
it 'filters issues by searched milestone and text' do
- search = "milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author, assignee and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author, assignee, label and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug"
- input_filtered_search(search)
-
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
+
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: bug_label.title }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -506,44 +597,6 @@ describe 'Filter issues', js: true, feature: true do
end
end
- describe 'overwrites selected filter' do
- it 'changes author' do
- input_filtered_search("author:@#{user.username}", submit: false)
-
- select_search_at_index(3)
-
- page.within '#js-dropdown-author' do
- click_button user2.username
- end
-
- expect(filtered_search.value).to eq("author:@#{user2.username} ")
- end
-
- it 'changes label' do
- input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
-
- select_search_at_index(27)
-
- page.within '#js-dropdown-label' do
- click_button label.name
- end
-
- expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
- end
-
- it 'changes label correctly space is in previous label' do
- input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
-
- select_search_at_index(0)
-
- page.within '#js-dropdown-label' do
- click_button label.name
- end
-
- expect(filtered_search.value).to eq("label:~#{label.name} ")
- end
- end
-
describe 'filter issues by text' do
context 'only text' do
it 'filters issues by searched text' do
@@ -605,80 +658,81 @@ describe 'Filter issues', js: true, feature: true do
context 'searched text with other filters' do
it 'filters issues by searched text and author' do
+ # After searching, all search terms are placed at the end
input_filtered_search("bug author:@#{user.username}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author and more text' do
input_filtered_search("bug author:@#{user.username} report")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} bug report")
+ expect_filtered_search_input('bug report')
end
it 'filters issues by searched text, author and assignee' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, more text and assignee' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
+ expect_filtered_search_input('bug report')
end
it 'filters issues by searched text, author, more text, assignee and even more text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with")
+ expect_filtered_search_input('bug report with')
end
it 'filters issues by searched text, author, assignee and label' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything")
+ expect_filtered_search_input('bug report with everything')
end
it 'filters issues by searched text, author, assignee, label and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you")
+ expect_filtered_search_input('bug report with everything you')
end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought")
+ expect_filtered_search_input('bug report with everything you thought')
end
end
@@ -717,8 +771,8 @@ describe 'Filter issues', js: true, feature: true do
before do
input_filtered_search('bug')
- # Wait for search results to load
- sleep 2
+ # This ensures that the search is performed
+ expect_issues_list_count(4, 1)
end
it 'open state' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 90eb60eb337..59244d65eec 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Search bar', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
new file mode 100644
index 00000000000..96e87c82d2c
--- /dev/null
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -0,0 +1,352 @@
+require 'rails_helper'
+
+describe 'Visual tokens', js: true, feature: true do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
+ let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
+ let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
+ let!(:label) { create(:label, project: project, title: 'abc') }
+ let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') }
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
+ let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
+ let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
+ let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ def is_input_focused
+ page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
+ end
+
+ before do
+ project.add_user(user, :master)
+ project.add_user(user_rock, :master)
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'editing author token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens author dropdown' do
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ describe 'selecting different author from dropdown' do
+ before do
+ filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing assignee token' do
+ before do
+ input_filtered_search('assignee:@root author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens assignee dropdown' do
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ describe 'selecting static option from dropdown' do
+ before do
+ find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing milestone token' do
+ before do
+ input_filtered_search('milestone:%10.0 author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens milestone dropdown' do
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'selects static option from dropdown' do
+ find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('%10.0')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+ end
+
+ describe 'editing label token' do
+ before do
+ input_filtered_search("label:~#{label.title} author:none", submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens label dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+
+ it 'selects option from dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input("~#{label.title}")
+ end
+
+ it 'filters value' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ filtered_search.send_keys(:backspace)
+
+ filter_label_dropdown.find('.filter-dropdown-item')
+
+ expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+ end
+
+ describe 'editing multiple tokens' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens author dropdown' do
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'opens assignee dropdown' do
+ find('.tokens-container .filtered-search-token', text: 'Assignee').double_click
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+ end
+
+ describe 'editing a search term while editing another filter token' do
+ before do
+ input_filtered_search('author assignee:', submit: false)
+ first('.tokens-container .filtered-search-term').double_click
+ end
+
+ it 'opens hint dropdown' do
+ expect(page).to have_css('#js-dropdown-hint', visible: true)
+ end
+
+ it 'opens author dropdown' do
+ find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click
+
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+ end
+
+ describe 'add new token after editing existing token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ filtered_search.send_keys(' ')
+ end
+
+ describe 'opens dropdowns' do
+ it 'opens hint dropdown' do
+ expect(page).to have_css('#js-dropdown-hint', visible: true)
+ end
+
+ it 'opens author dropdown' do
+ filtered_search.send_keys('author:')
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'opens assignee dropdown' do
+ filtered_search.send_keys('assignee:')
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'opens milestone dropdown' do
+ filtered_search.send_keys('milestone:')
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'opens label dropdown' do
+ filtered_search.send_keys('label:')
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+ end
+
+ describe 'creates visual tokens' do
+ it 'creates author token' do
+ filtered_search.send_keys('author:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Author')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates assignee token' do
+ filtered_search.send_keys('assignee:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Assignee')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates milestone token' do
+ filtered_search.send_keys('milestone:none ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Milestone')
+ expect(token.find('.value').text).to eq('none')
+ end
+
+ it 'creates label token' do
+ filtered_search.send_keys('label:~Backend ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Label')
+ expect(token.find('.value').text).to eq('~Backend')
+ end
+ end
+
+ it 'does not tokenize incomplete token' do
+ filtered_search.send_keys('author:')
+
+ find('#content-body').click
+ token = page.all('.tokens-container .js-visual-token')[1]
+
+ expect_filtered_search_input_empty
+ expect(token.find('.name').text).to eq('Author')
+ end
+ end
+
+ describe 'search using incomplete visual tokens' do
+ before do
+ input_filtered_search('author:@root assignee:none', extra_space: false)
+ end
+
+ it 'tokenizes the search term to complete visual token' do
+ expect_tokens([
+ { name: 'author', value: '@root' },
+ { name: 'assignee', value: 'none' }
+ ])
+ end
+ end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 741ca95f1ca..755992069ff 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -1,8 +1,11 @@
require 'rails_helper'
describe 'New/edit issue', feature: true, js: true do
+ include GitlabRoutingHelper
+
let!(:project) { create(:project) }
let!(:user) { create(:user)}
+ let!(:user2) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
@@ -10,6 +13,7 @@ describe 'New/edit issue', feature: true, js: true do
before do
project.team << [user, :master]
+ project.team << [user2, :master]
login_as(user)
end
@@ -22,14 +26,23 @@ describe 'New/edit issue', feature: true, js: true do
fill_in 'issue_title', with: 'title'
fill_in 'issue_description', with: 'title'
+ expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Assignee'
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user2.name
+ end
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user2.name
end
+ expect(find('a', text: 'Assign to me')).to be_visible
+
+ click_link 'Assign to me'
expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
click_button 'Milestone'
page.within '.issue-milestone' do
@@ -67,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do
expect(page).to have_content label2.title
end
end
+
+ page.within '.issuable-meta' do
+ issue = Issue.find_by(title: 'title')
+
+ expect(page).to have_text("Issue #{issue.to_reference}")
+ # compare paths because the host differ in test
+ expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
+ end
end
it 'correctly updates the dropdown toggle when removing a label' do
@@ -94,6 +115,7 @@ describe 'New/edit issue', feature: true, js: true do
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 094f645a077..a58aedc924e 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe 'Issues', feature: true do
+ include DropzoneHelper
include IssueHelpers
include SortingHelper
include WaitForAjax
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
before do
login_as :user
@@ -150,7 +151,7 @@ describe 'Issues', feature: true do
describe 'Filter issue' do
before do
- ['foobar', 'barbaz', 'gitlab'].each do |title|
+ %w(foobar barbaz gitlab).each do |title|
create(:issue,
author: @user,
assignee: @user,
@@ -564,19 +565,40 @@ describe 'Issues', feature: true do
end
describe 'new issue' do
+ context 'by unauthenticated user' do
+ before do
+ logout
+ end
+
+ it 'redirects to signin then back to new issue after signin' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ click_link 'New issue'
+
+ expect(current_path).to eq new_user_session_path
+
+ login_as :user
+
+ expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project)
+ end
+ end
+
context 'dropzone upload file', js: true do
before do
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'uploads file when dragging into textarea' do
- drop_in_dropzone test_image_file
-
- # Wait for the file to upload
- sleep 1
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
+
+ it 'adds double newline to end of attachment markdown' do
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+
+ expect(page.find_field("issue_description").value).to match /\n\n$/
+ end
end
end
@@ -656,25 +678,4 @@ describe 'Issues', feature: true do
end
end
end
-
- def drop_in_dropzone(file_path)
- # Generate a fake input selector
- page.execute_script <<-JS
- var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
- ).appendTo('body');
- JS
- # Attach the file to the fake input selector with Capybara
- attach_file("fakeFileInput", file_path)
- # Add the file to a fileList array and trigger the fake drop event
- page.execute_script <<-JS
- var fileList = [$('#fakeFileInput')[0].files[0]];
- var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
- $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
- JS
- end
-
- def test_image_file
- File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- end
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index ae609160e18..f32d1f78b40 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -48,6 +48,18 @@ feature 'Login', feature: true do
end
end
+ describe 'with the ghost user' do
+ it 'disallows login' do
+ login_with(User.ghost)
+
+ expect(page).to have_content('Invalid Login or password.')
+ end
+
+ it 'does not update Devise trackable attributes' do
+ expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count }
+ end
+ end
+
describe 'with two-factor authentication' do
def enter_code(code)
fill_in 'user_otp_attempt', with: code
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 32159559c37..894df13a2dc 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -115,6 +115,14 @@ describe 'GitLab Markdown', feature: true do
expect(doc).to have_selector('span:contains("span tag")')
end
+ it 'permits details elements' do
+ expect(doc).to have_selector('details:contains("Hiding the details")')
+ end
+
+ it 'permits summary elements' do
+ expect(doc).to have_selector('details summary:contains("collapsible")')
+ end
+
it 'permits style attribute in th elements' do
aggregate_failures do
expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index d710a780111..18508a44184 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -154,7 +154,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
- }
+ }.freeze
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
context description do
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 73c5ef31edc..18833ba7266 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
expect(page).to have_content pipeline.status
expect(page).to have_content pipeline.id
end
-
- expect(page.find('a.btn-remove')[:href])
- .to include fork_project.path_with_namespace
end
end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
new file mode 100644
index 00000000000..a6c72b0b3ac
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -0,0 +1,186 @@
+require 'spec_helper'
+
+feature 'Diff note avatars', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'discussion tab' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show avatars on discussion tab' do
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+
+ it 'does not render avatars after commening on discussion tab' do
+ click_button 'Reply...'
+
+ page.within('.js-discussion-note-form') do
+ find('.note-textarea').native.send_keys('Test comment')
+
+ click_button 'Comment'
+ end
+
+ expect(page).to have_content('Test comment')
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+ end
+
+ context 'commit view' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, merge_request.commits.first.id)
+ end
+
+ it 'does not render avatar after commenting' do
+ first('.diff-line-num').trigger('mouseover')
+ find('.js-add-diff-note-button').click
+
+ page.within('.js-discussion-note-form') do
+ find('.note-textarea').native.send_keys('test comment')
+
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_content('test comment')
+ expect(page).not_to have_selector('.js-avatar-container')
+ expect(page).not_to have_selector('.diff-comment-avatar-holders')
+ end
+ end
+
+ %w(inline parallel).each do |view|
+ context "#{view} view" do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+ wait_for_ajax
+ end
+
+ it 'shows note avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+ end
+ end
+
+ it 'shows comment on note avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ end
+ end
+
+ it 'toggles comments when clicking avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+ end
+
+ expect(page).to have_selector('.notes_holder', visible: false)
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ first('img.js-diff-comment-avatar').click
+ end
+
+ expect(page).to have_selector('.notes_holder')
+ end
+
+ it 'removes avatar when note is deleted' do
+ page.within find(".note-row-#{note.id}") do
+ find('.js-note-delete').click
+ end
+
+ wait_for_ajax
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ expect(page).not_to have_selector('img.js-diff-comment-avatar')
+ end
+ end
+
+ it 'adds avatar when commenting' do
+ click_button 'Reply...'
+
+ page.within '.js-discussion-note-form' do
+ find('.js-note-text').native.send_keys('Test')
+
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+ end
+ end
+
+ it 'adds multiple comments' do
+ 3.times do
+ click_button 'Reply...'
+
+ page.within '.js-discussion-note-form' do
+ find('.js-note-text').native.send_keys('Test')
+
+ find('.js-comment-button').trigger 'click'
+
+ wait_for_ajax
+ end
+ end
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+ expect(find('.diff-comments-more-count')).to have_content '+1'
+ end
+ end
+
+ context 'multiple comments' do
+ before do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+ wait_for_ajax
+ end
+
+ it 'shows extra comment count' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(find('.diff-comments-more-count')).to have_content '+1'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 5608cda28f8..265a0cfc198 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
input_filtered_search('milestone:none')
+ expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_filtered_search_input_empty
+
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 6579a88d4ab..70e3997e716 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe 'for assignee from mr#index' do
let(:search_query) { "assignee:@#{user.username}" }
+ def expect_assignee_visual_tokens
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
+ end
+
before do
input_filtered_search(search_query)
@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context 'assignee', js: true do
it 'updates to current user' do
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
end
end
describe 'for milestone from mr#index' do
- let(:search_query) { "milestone:%#{milestone.title}" }
+ let(:search_query) { "milestone:%\"#{milestone.title}\"" }
+
+ def expect_milestone_visual_tokens
+ expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
+ expect_filtered_search_input_empty
+ end
before do
input_filtered_search(search_query)
@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context 'milestone', js: true do
it 'updates to current milestone' do
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
end
end
@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search('label:none')
expect_mr_list_count(1)
- expect_filtered_search_input('label:none')
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
it 'filters by a label' do
input_filtered_search("label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~#{label.title}")
+ expect_tokens([{ name: 'label', value: "~#{label.title}" }])
+ expect_filtered_search_input_empty
end
it "filters by `won't fix` and another label" do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ expect_tokens([
+ { name: 'label', value: "~\"#{wontfix.title}\"" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
end
it "filters by `won't fix` label followed by another label after page load" do
input_filtered_search("label:~\"#{wontfix.title}\"")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\"")
-
- input_filtered_search_keys(" label:~#{label.title}")
+ expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
+ expect_filtered_search_input_empty
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ input_filtered_search_keys("label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ expect_tokens([
+ { name: 'label', value: "~\"#{wontfix.title}\"" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
end
end
@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search("assignee:@#{user.username}")
expect_mr_list_count(1)
- expect_filtered_search_input("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
- input_filtered_search_keys(" label:~#{label.title}")
+ input_filtered_search_keys("label:~#{label.title} ")
expect_mr_list_count(1)
@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end
context 'assignee and label', js: true do
+ def expect_assignee_label_visual_tokens
+ expect_tokens([
+ { name: 'assignee', value: "@#{user.username}" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
+ end
+
it 'updates to current assignee and label' do
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
end
end
@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1)
+ expect_tokens([{ name: 'label', value: '~bug' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and milestone' do
@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1)
+ expect_tokens([{ name: 'milestone', value: '%8' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and assignee' do
@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1)
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and author' do
@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" author:@#{user.username}")
expect_mr_list_count(1)
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input('Bug')
end
end
end
@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
- expect_filtered_search_input("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'filter by new user' do
@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
- expect_filtered_search_input("assignee:@#{new_user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
+ expect_filtered_search_input_empty
end
end
@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
- expect_filtered_search_input("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'filter by new user' do
@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
- expect_filtered_search_input("author:@#{new_user.username}")
+ expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
+ expect_filtered_search_input_empty
end
end
end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 7594cbf54e8..f8518f450dc 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -1,15 +1,19 @@
require 'rails_helper'
describe 'New/edit merge request', feature: true, js: true do
+ include GitlabRoutingHelper
+
let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:fork_project) { create(:project, forked_from_project: project) }
let!(:user) { create(:user)}
+ let!(:user2) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
before do
project.team << [user, :master]
+ project.team << [user2, :master]
end
context 'owned projects' do
@@ -33,8 +37,14 @@ describe 'New/edit merge request', feature: true, js: true do
it 'creates new merge request' do
click_button 'Assignee'
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user2.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user2.name
end
+
+ click_link 'Assign to me'
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
@@ -76,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do
expect(page).to have_content label2.title
end
end
+
+ page.within '.issuable-meta' do
+ merge_request = MergeRequest.find_by(source_branch: 'fix')
+
+ expect(page).to have_text("Merge Request #{merge_request.to_reference}")
+ # compare paths because the host differ in test
+ expect(find_link(merge_request.to_reference)[:href])
+ .to end_with(merge_request_path(merge_request))
+ end
end
end
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index f2f8f11ab28..0ceaf7bc830 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -34,7 +34,7 @@ feature 'Merge immediately', :feature, :js do
click_link 'Merge Immediately'
- expect(find('.js-merge-button')).to have_content('Merge in progress')
+ expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress')
end
end
end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index 2ea9c317bd1..ed7193b9777 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -75,7 +75,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
context 'when it was enabled and then canceled' do
let(:merge_request) do
create(:merge_request_with_diffs,
- :merge_when_build_succeeds,
+ :merge_when_pipeline_succeeds,
source_project: project,
title: 'Bug NS-04',
author: user,
@@ -97,7 +97,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
author: user,
merge_user: user,
title: 'MepMep',
- merge_when_build_succeeds: true)
+ merge_when_pipeline_succeeds: true)
end
let!(:build) do
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index d2f5c4afc93..447764566e0 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -27,9 +27,9 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
status: status)
end
- context 'when merge requests can only be merged if the build succeeds' do
+ context 'when merge requests can only be merged if the pipeline succeeds' do
before do
- project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+ project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
end
context 'when CI is running' do
@@ -88,7 +88,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
context 'when merge requests can be merged when the build failed' do
before do
- project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+ project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
end
context 'when CI is running' do
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 58f11499e3f..14511707af4 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues filter reset button', feature: true, js: true do
+feature 'Merge requests filter clear button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax
@@ -24,67 +24,113 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do
visit_merge_requests(project, milestone_title: milestone.title)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when a label filter has been applied' do
it 'resets the label filter' do
visit_merge_requests(project, label_name: bug.name)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
+ end
+ end
+
+ context 'when multiple label filters have been applied' do
+ let!(:label) { create(:label, project: project, name: 'Frontend') }
+ let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ before do
+ visit_merge_requests(project)
+ init_label_search
+ end
+
+ it 'filters bug label' do
+ filtered_search.set('~bug')
+
+ filter_dropdown.find('.filter-dropdown-item', text: bug.title).click
+ init_label_search
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
end
end
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug')
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when author filter has been applied' do
it 'resets the author filter' do
visit_merge_requests(project, author_username: user.username)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
visit_merge_requests(project, assignee_username: user.username)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when all filters have been applied' do
- it 'resets all filters' do
+ it 'clears all filters' do
visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+
expect(page).to have_css(merge_request_css, count: 0)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when no filters have been applied' do
- it 'the reset link should not be visible' do
+ it 'the clear button should not be visible' do
visit_merge_requests(project)
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
expect(page).not_to have_css(clear_search_css)
end
end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 2f3c3e45ae6..a1f4eb2688b 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it 'changes target_branch in new merge_request' do
visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
- click_button "Compare branches and continue"
fill_in "merge_request_title", with: 'My brand new feature'
fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 4ad944366c8..c2db7d8da3c 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -3,8 +3,8 @@ require 'rails_helper'
describe 'Merge request', :feature, :js do
include WaitForAjax
- let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
before do
@@ -31,13 +31,18 @@ describe 'Merge request', :feature, :js do
wait_for_ajax
- expect(page).to have_selector('.accept_merge_request')
+ expect(page).to have_selector('.accept-merge-request')
end
end
context 'view merge request' do
let!(:environment) { create(:environment, project: project) }
- let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) }
+
+ let!(:deployment) do
+ create(:deployment, environment: environment,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha)
+ end
before do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
@@ -51,6 +56,89 @@ describe 'Merge request', :feature, :js do
expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
end
end
+
+ it 'shows green accept merge request button' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+ expect(page).to have_selector('.accept-merge-request.btn-create')
+ end
+ end
+
+ context 'view merge request with external CI service' do
+ before do
+ create(:service, project: project,
+ active: true,
+ type: 'CiService',
+ category: 'ci')
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'has danger button while waiting for external CI status' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+ expect(page).to have_selector('.accept-merge-request.btn-danger')
+ end
+ end
+
+ context 'view merge request with failed GitLab CI pipelines' do
+ before do
+ commit_status = create(:commit_status, project: project, status: 'failed')
+ pipeline = create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: 'failed',
+ statuses: [commit_status])
+ create(:ci_build, :pending, pipeline: pipeline)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'has danger button when not succeeded' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+ expect(page).to have_selector('.accept-merge-request.btn-danger')
+ end
+ end
+
+ context 'when merge request is in the blocked pipeline state' do
+ before do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: :manual)
+
+ visit namespace_project_merge_request_path(project.namespace,
+ project,
+ merge_request)
+ end
+
+ it 'shows information about blocked pipeline' do
+ expect(page).to have_content("Pipeline blocked")
+ expect(page).to have_content(
+ "The pipeline for this merge request requires a manual action")
+ expect(page).to have_css('.ci-status-icon-manual')
+ end
+ end
+
+ context 'view merge request with MWBS button' do
+ before do
+ commit_status = create(:commit_status, project: project, status: 'pending')
+ pipeline = create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: 'pending',
+ statuses: [commit_status])
+ create(:ci_build, :pending, pipeline: pipeline)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'has info button when MWBS button' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_ajax
+ expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info')
+ end
end
context 'merge error' do
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index a2e40546588..c3297de709a 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -24,7 +24,7 @@ feature 'Milestone', feature: true do
find('input[name="commit"]').click
expect(find('.alert-success')).to have_content('Assign some issues to this milestone.')
- expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016')
+ expect(page).to have_content('Nov 16, 2016–Dec 16, 2016')
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 7a562b5e03d..e63feb14b7e 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do
let(:user) { create(:user) }
before do
- login_as :user
+ login_as(user)
end
describe 'when signup is enabled' do
@@ -16,7 +16,7 @@ describe 'Profile account page', feature: true do
it { expect(page).to have_content('Remove account') }
it 'deletes the account' do
- expect { click_link 'Delete account' }.to change { User.count }.by(-1)
+ expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1)
expect(current_path).to eq(new_user_session_path)
end
end
@@ -61,4 +61,18 @@ describe 'Profile account page', feature: true do
expect(find('#incoming-email-token').value).not_to eq(previous_token)
end
end
+
+ describe 'when I change my username' do
+ before do
+ visit profile_account_path
+ end
+
+ it 'changes my username' do
+ fill_in 'user_username', with: 'new-username'
+
+ click_button('Update username')
+
+ expect(page).to have_content('new-username')
+ end
+ end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index eb1050d21c6..2f436f153aa 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key))
- expect(find_field('Title').value).to eq 'dummy@gitlab.com'
+ expect(page).to have_field("Title", with: "dummy@gitlab.com")
end
scenario 'saves the new key' do
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index eb7b8a24669..0917d4dc3ef 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -4,11 +4,11 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
let(:user) { create(:user) }
def active_personal_access_tokens
- find(".table.active-personal-access-tokens")
+ find(".table.active-tokens")
end
def inactive_personal_access_tokens
- find(".table.inactive-personal-access-tokens")
+ find(".table.inactive-tokens")
end
def created_personal_access_token
@@ -26,7 +26,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
end
describe "token creation" do
- it "allows creation of a token" do
+ it "allows creation of a personal access token" do
name = FFaker::Product.brand
visit profile_personal_access_tokens_path
@@ -43,7 +43,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
click_on "Create Personal Access Token"
expect(active_personal_access_tokens).to have_text(name)
- expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ expect(active_personal_access_tokens).to have_text('In')
expect(active_personal_access_tokens).to have_text('api')
expect(active_personal_access_tokens).to have_text('read_user')
end
@@ -60,6 +60,18 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
end
end
+ describe 'active tokens' do
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'only shows personal access tokens' do
+ visit profile_personal_access_tokens_path
+
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(active_personal_access_tokens).not_to have_text(impersonation_token.name)
+ end
+ end
+
describe "inactive tokens" do
let!(:personal_access_token) { create(:personal_access_token, user: user) }
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
deleted file mode 100644
index e05fbb3715c..00000000000
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do
- let(:user) { create(:user) }
-
- before do
- login_as(user)
- end
-
- scenario 'User opts into receiving notifications about their own activity' do
- visit profile_notifications_path
-
- expect(page).not_to have_checked_field('user[notified_of_own_activity]')
-
- check 'user[notified_of_own_activity]'
-
- expect(page).to have_content('Notification settings saved')
- expect(page).to have_checked_field('user[notified_of_own_activity]')
- end
-
- scenario 'User opts out of receiving notifications about their own activity' do
- user.update!(notified_of_own_activity: true)
- visit profile_notifications_path
-
- expect(page).to have_checked_field('user[notified_of_own_activity]')
-
- uncheck 'user[notified_of_own_activity]'
-
- expect(page).to have_content('Notification settings saved')
- expect(page).not_to have_checked_field('user[notified_of_own_activity]')
- end
-end
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
new file mode 100644
index 00000000000..b47c6d431eb
--- /dev/null
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Project Activity RSS' do
+ let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:path) { activity_namespace_project_path(project.namespace, project) }
+
+ before do
+ create(:issue, project: project)
+ end
+
+ context 'when signed in' do
+ before do
+ user = create(:user)
+ project.team << [user, :developer]
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button without a private token"
+ end
+end
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
new file mode 100644
index 00000000000..d94204230f6
--- /dev/null
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, js: true do
+ include TreeHelper
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:path) { 'CHANGELOG' }
+ let(:sha) { project.repository.commit.sha }
+
+ describe 'On a file(blob)' do
+ def get_absolute_url(path = "")
+ "http://#{page.server.host}:#{page.server.port}#{path}"
+ end
+
+ def visit_blob(fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ describe 'Click "Permalink" button' do
+ it 'works with no initial line number fragment hash' do
+ visit_blob
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))))
+ end
+
+ it 'maintains intitial fragment hash' do
+ fragment = "L3"
+
+ visit_blob(fragment)
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)))
+ end
+
+ it 'changes fragment hash if line number clicked' do
+ ending_fragment = "L5"
+
+ visit_blob
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+ end
+
+ it 'with initial fragment hash, changes fragment hash if line number clicked' do
+ fragment = "L1"
+ ending_fragment = "L5"
+
+ visit_blob(fragment)
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: ending_fragment)))
+ end
+ end
+
+ describe 'Click "Blame" button' do
+ it 'works with no initial line number fragment hash' do
+ visit_blob
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path))))
+ end
+
+ it 'maintains intitial fragment hash' do
+ fragment = "L3"
+
+ visit_blob(fragment)
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: fragment)))
+ end
+
+ it 'changes fragment hash if line number clicked' do
+ ending_fragment = "L5"
+
+ visit_blob
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+ end
+
+ it 'with initial fragment hash, changes fragment hash if line number clicked' do
+ fragment = "L1"
+ ending_fragment = "L5"
+
+ visit_blob(fragment)
+
+ find('#L3').click
+ find("##{ending_fragment}").click
+
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(namespace_project_blame_path(project.namespace, project, tree_join('master', path), anchor: ending_fragment)))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
new file mode 100644
index 00000000000..03d08c12612
--- /dev/null
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'New blob creation', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:project) { create(:project) }
+ given(:content) { 'class NextFeature\nend\n' }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_new_blob_path(project.namespace, project, 'master')
+ end
+
+ def edit_file
+ wait_for_ajax
+ fill_in 'file_name', with: 'feature.rb'
+ execute_script("ace.edit('editor').setValue('#{content}')")
+ end
+
+ def select_branch_index(index)
+ first('button.js-target-branch').click
+ wait_for_ajax
+ all('a[data-group="Branches"]')[index].click
+ end
+
+ def create_new_branch(name)
+ first('button.js-target-branch').click
+ click_link 'Create new branch'
+ fill_in 'new_branch_name', with: name
+ click_button 'Create'
+ end
+
+ def commit_file
+ click_button 'Commit Changes'
+ end
+
+ context 'with default target branch' do
+ background do
+ edit_file
+ commit_file
+ end
+
+ scenario 'creates the blob in the default branch' do
+ expect(page).to have_content 'master'
+ expect(page).to have_content 'successfully created'
+ expect(page).to have_content 'NextFeature'
+ end
+ end
+
+ context 'with different target branch' do
+ background do
+ edit_file
+ select_branch_index(0)
+ commit_file
+ end
+
+ scenario 'creates the blob in the different branch' do
+ expect(page).to have_content 'test'
+ expect(page).to have_content 'successfully created'
+ end
+ end
+
+ context 'with a new target branch' do
+ given(:new_branch_name) { 'new-feature' }
+
+ background do
+ edit_file
+ create_new_branch(new_branch_name)
+ commit_file
+ end
+
+ scenario 'creates the blob in the new branch' do
+ expect(page).to have_content new_branch_name
+ expect(page).to have_content 'successfully created'
+ end
+ scenario 'returns you to the mr' do
+ expect(page).to have_content 'New Merge Request'
+ expect(page).to have_content "From #{new_branch_name} into master"
+ expect(page).to have_content 'Add new file'
+ end
+ end
+
+ context 'the file already exist in the source branch' do
+ background do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: 'master',
+ target_branch: 'master',
+ commit_message: 'Create file',
+ file_path: 'feature.rb',
+ file_content: content
+ ).execute
+ edit_file
+ commit_file
+ end
+
+ scenario 'shows error message' do
+ expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
+ expect(page).to have_content('New File')
+ expect(page).to have_content('NextFeature')
+ end
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index d26a0caf036..8e0306ce83b 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -17,6 +17,14 @@ describe 'Branches', feature: true do
repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
expect(page).to have_content("Protected branches can be managed in project settings")
end
+
+ it 'avoids a N+1 query in branches index' do
+ control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count
+
+ %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') }
+
+ expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+ end
end
describe 'Find branches' do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7baf7913424..0b972d2a439 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -60,6 +60,7 @@ describe 'Cherry-pick Commits' do
click_button 'Cherry-pick'
end
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
+ expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
end
end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..30a2b2bcf8c
--- /dev/null
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph in Commit View', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ login_as(user)
+ end
+
+ context 'when commit has pipelines' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ let(:build) do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ build.run
+ visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ it 'should show the builds list when stage is clicked' do
+ first('.mini-pipeline-graph-dropdown-toggle').click
+
+ wait_for_ajax
+
+ page.within '.js-builds-dropdown-list' do
+ expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_content(build.stage)
+ end
+ end
+ end
+
+ context 'when commit does not have pipelines' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+ end
+
+ it 'should not display a mini pipeline graph' do
+ expect(page).not_to have_selector('.mr-widget-pipeline-graph')
+ end
+ end
+end
diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb
new file mode 100644
index 00000000000..6e0e1916f87
--- /dev/null
+++ b/spec/features/projects/commit/rss_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+feature 'Project Commits RSS' do
+ let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:path) { namespace_project_commits_path(project.namespace, project, :master) }
+
+ context 'when signed in' do
+ before do
+ user = create(:user)
+ project.team << [user, :developer]
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..030043d14aa 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -26,6 +26,14 @@ describe "Compare", js: true do
click_button "Compare"
expect(page).to have_content "Commits"
end
+
+ it "filters branches" do
+ select_using_dropdown("from", "wip")
+
+ find(".js-compare-from-dropdown .compare-dropdown-toggle").click
+
+ expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3)
+ end
end
describe "tags" do
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index 0c51fe72ca4..2352329d58c 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do
end
def expect_instructions_for(protocol)
- msg = :"#{protocol.downcase}_url_to_repo"
-
- expect(page).to have_content("git clone #{project.send(msg)}")
+ url =
+ case protocol
+ when 'ssh'
+ project.ssh_url_to_repo
+ when 'http'
+ project.http_url_to_repo(developer)
+ end
+
+ expect(page).to have_content("git clone #{url}")
end
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index a1643fd1f43..7c319af893b 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
- it 'hides merge requests section after save' do
- select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
-
- expect(page).to have_selector('.merge-requests-feature', visible: false)
-
- click_button 'Save changes'
+ context 'given project with merge_requests_disabled access level' do
+ let(:project) { create(:project, :merge_requests_disabled) }
- wait_for_ajax
-
- expect(page).to have_selector('.merge-requests-feature', visible: false)
+ it 'hides merge requests section' do
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
end
end
context 'builds select' do
- it 'hides merge requests section' do
+ it 'hides builds select section' do
select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
expect(page).to have_selector('.builds-feature', visible: false)
end
- it 'hides merge requests section after save' do
- select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
-
- expect(page).to have_selector('.builds-feature', visible: false)
+ context 'given project with builds_disabled access level' do
+ let(:project) { create(:project, :builds_disabled) }
- click_button 'Save changes'
-
- wait_for_ajax
-
- expect(page).to have_selector('.builds-feature', visible: false)
+ it 'hides builds select section' do
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
end
end
end
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
new file mode 100644
index 00000000000..ee925e811e1
--- /dev/null
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'Environment > Metrics', :feature do
+ include PrometheusHelpers
+
+ given(:user) { create(:user) }
+ given(:project) { create(:prometheus_project) }
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:environment) { create(:environment, project: project) }
+ given(:current_time) { Time.now.utc }
+
+ background do
+ project.add_developer(user)
+ create(:deployment, environment: environment, deployable: build)
+ stub_all_prometheus_requests(environment.slug)
+
+ login_as(user)
+ visit_environment(environment)
+ end
+
+ around do |example|
+ Timecop.freeze(current_time) { example.run }
+ end
+
+ context 'with deployments and related deployable present' do
+ scenario 'shows metrics' do
+ click_link('See metrics')
+
+ expect(page).to have_css('svg.prometheus-graph')
+ end
+ end
+
+ def visit_environment(environment)
+ visit namespace_project_environment_path(environment.project.namespace,
+ environment.project,
+ environment)
+ end
+end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
new file mode 100644
index 00000000000..e2d16e0830a
--- /dev/null
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -0,0 +1,220 @@
+require 'spec_helper'
+
+feature 'Environment', :feature do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ feature 'environment details page' do
+ given!(:environment) { create(:environment, project: project) }
+ given!(:deployment) { }
+ given!(:action) { }
+
+ before do
+ visit_environment(environment)
+ end
+
+ scenario 'shows environment name' do
+ expect(page).to have_content(environment.name)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'with deployments' do
+ context 'when there is no related deployable' do
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: nil)
+ end
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ expect(page).not_to have_link('Re-deploy')
+ expect(page).not_to have_terminal_button
+ end
+ end
+
+ context 'with related deployable present' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ scenario 'does show build name' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ expect(page).to have_link('Re-deploy')
+ expect(page).not_to have_terminal_button
+ end
+
+ context 'with manual action' do
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'deploy to production')
+ end
+
+ scenario 'does show a play button' do
+ expect(page).to have_link(action.name.humanize)
+ end
+
+ scenario 'does allow to play manual action' do
+ expect(action).to be_manual
+
+ expect { click_link(action.name.humanize) }
+ .not_to change { Ci::Pipeline.count }
+
+ expect(page).to have_content(action.name)
+ expect(action.reload).to be_pending
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with terminal' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
+
+ context 'for project master' do
+ let(:role) { :master }
+
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+
+ context 'web terminal', :js do
+ before do
+ # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
+ allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ visit terminal_namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ it 'displays a web terminal' do
+ expect(page).to have_selector('#terminal')
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+ end
+
+ context 'for developer' do
+ let(:role) { :developer }
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+ end
+ end
+
+ context 'when environment is available' do
+ context 'with stop action' do
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'close_app')
+ end
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
+
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
+
+ context 'without stop action' do
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+ end
+ end
+ end
+
+ context 'when environment is stopped' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
+ end
+ end
+ end
+
+ feature 'auto-close environment when branch is deleted' do
+ given(:project) { create(:project) }
+
+ given!(:environment) do
+ create(:environment, :with_review_app, project: project,
+ ref: 'feature')
+ end
+
+ scenario 'user visits environment page' do
+ visit_environment(environment)
+
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'user deletes the branch with running environment' do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ remove_branch_with_hooks(project, user, 'feature') do
+ page.within('.js-branch-feature') { find('a.btn-remove').click }
+ end
+
+ visit_environment(environment)
+
+ expect(page).to have_no_link('Stop')
+ end
+
+ ##
+ # This is a workaround for problem described in #24543
+ #
+ def remove_branch_with_hooks(project, user, branch)
+ params = {
+ oldrev: project.commit(branch).id,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "refs/heads/#{branch}"
+ }
+
+ yield
+
+ GitPushService.new(project, user, params).execute
+ end
+ end
+
+ def visit_environment(environment)
+ visit namespace_project_environment_path(environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
+ def have_terminal_button
+ have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
+ end
+end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
new file mode 100644
index 00000000000..641e2cf7402
--- /dev/null
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -0,0 +1,244 @@
+require 'spec_helper'
+
+feature 'Environments page', :feature, :js do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ project.team << [user, role]
+ login_as(user)
+ end
+
+ given!(:environment) { }
+ given!(:deployment) { }
+ given!(:action) { }
+
+ before do
+ visit_environments(project)
+ end
+
+ describe 'page tabs' do
+ scenario 'shows "Available" and "Stopped" tab with links' do
+ expect(page).to have_link('Available')
+ expect(page).to have_link('Stopped')
+ end
+ end
+
+ context 'without environments' do
+ scenario 'does show no environments' do
+ expect(page).to have_content('You don\'t have any environments right now.')
+ end
+
+ scenario 'does show 0 as counter for environments in both tabs' do
+ expect(page.find('.js-available-environments-count').text).to eq('0')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
+ end
+
+ describe 'when showing the environment' do
+ given(:environment) { create(:environment, project: project) }
+
+ scenario 'does show environment name' do
+ expect(page).to have_link(environment.name)
+ end
+
+ scenario 'does show number of available and stopped environments' do
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('No deployments yet')
+ end
+
+ context 'for available environment' do
+ given(:environment) { create(:environment, project: project, state: :available) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+
+ context 'for stopped environment' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+ end
+
+ context 'with deployments' do
+ given(:project) { create(:project) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ sha: project.commit.id)
+ end
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
+ end
+
+ context 'with build and manual actions' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
+ end
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ sha: project.commit.id)
+ end
+
+ scenario 'does show a play button' do
+ find('.js-dropdown-play-icon-container').click
+ expect(page).to have_content(action.name.humanize)
+ end
+
+ scenario 'does allow to play manual action', js: true do
+ expect(action).to be_manual
+
+ find('.js-dropdown-play-icon-container').click
+ expect(page).to have_content(action.name.humanize)
+
+ expect { find('.js-manual-action-link').click }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} ##{build.id}")
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+
+ scenario 'does not show external link button' do
+ expect(page).not_to have_css('external-url')
+ end
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with stop action' do
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
+
+ scenario 'does show stop button' do
+ expect(page).to have_selector('.stop-env-link')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+ end
+
+ context 'with terminal' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
+
+ context 'for project master' do
+ let(:role) { :master }
+
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'for developer' do
+ let(:role) { :developer }
+
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
+ end
+ end
+ end
+ end
+ end
+
+ scenario 'does have a New environment button' do
+ expect(page).to have_link('New environment')
+ end
+
+ describe 'when creating a new environment' do
+ before do
+ visit_environments(project)
+ end
+
+ context 'when logged as developer' do
+ before do
+ click_link 'New environment'
+ end
+
+ context 'for valid name' do
+ before do
+ fill_in('Name', with: 'production')
+ click_on 'Save'
+ end
+
+ scenario 'does create a new pipeline' do
+ expect(page).to have_content('production')
+ end
+ end
+
+ context 'for invalid name' do
+ before do
+ fill_in('Name', with: 'name,with,commas')
+ click_on 'Save'
+ end
+
+ scenario 'does show errors' do
+ expect(page).to have_content('Name can contain only letters')
+ end
+ end
+ end
+
+ context 'when logged as reporter' do
+ given(:role) { :reporter }
+
+ scenario 'does not have a New environment link' do
+ expect(page).not_to have_link('New environment')
+ end
+ end
+ end
+
+ def have_terminal_button
+ have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
+ end
+
+ def visit_environments(project)
+ visit namespace_project_environments_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index 69295e450d0..d281043caa3 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user checks git blame', feature: true do
+feature 'user browses project', feature: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -18,4 +18,16 @@ feature 'user checks git blame', feature: true do
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
end
+
+ scenario 'can see raw content of LFS pointer with LFS disabled' do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ click_link 'files'
+ click_link 'lfs'
+ click_link 'lfs_object.iso'
+
+ expect(page).not_to have_content 'Download (1.5 MB)'
+ expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
+ expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
+ expect(page).to have_content 'size 1575078'
+ end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 64094af29c0..ccadc936567 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
- project.repository.remove_file(project_master, 'LICENSE',
+ project.repository.delete_file(project_master, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
@@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
@@ -49,7 +49,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -57,7 +57,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 4453b6d485f..420db962318 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -34,7 +34,7 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
index 8120a51c515..726469daba4 100644
--- a/spec/features/projects/guest_navigation_menu_spec.rb
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -15,13 +15,11 @@ describe "Guest navigation menu" do
within(".nav-links") do
expect(page).to have_content 'Project'
- expect(page).to have_content 'Activity'
expect(page).to have_content 'Issues'
expect(page).to have_content 'Wiki'
expect(page).not_to have_content 'Repository'
expect(page).not_to have_content 'Pipelines'
- expect(page).not_to have_content 'Graphs'
expect(page).not_to have_content 'Merge Requests'
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 16dddb2a86b..40caf89dd54 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -9,7 +9,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
include ExportFileHelper
let(:user) { create(:admin) }
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:sensitive_words) { %w[pass secret token key] }
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 3015576f6f8..2d1106ea3e8 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -4,7 +4,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
include Select2Helper
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index d0bafc6168c..cb399ea55df 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:project) { create(:empty_project) }
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 20cdfbae24f..399c1d478c5 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index e90a033b8c4..62d0aedda48 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
user,
'.gitlab/issue_templates/test.md',
longtemplate_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
@@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
background do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
- project.repository.commit_file(
+ project.repository.create_file(
fork_user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
new file mode 100644
index 00000000000..71429f00095
--- /dev/null
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+feature 'Project Issues RSS' do
+ let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:path) { namespace_project_issues_path(project.namespace, project) }
+
+ before do
+ create(:issue, project: project)
+ end
+
+ context 'when signed in' do
+ before do
+ user = create(:user)
+ project.team << [user, :developer]
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 81b0c991d4f..e2911a37e40 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -29,15 +29,15 @@ feature 'Issue prioritization', feature: true do
issue_1.labels << label_5
login_as user
- visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+ visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
# Ensure we are indicating that issues are sorted by priority
- expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+ expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
- expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1'])
+ expect(issue_titles).to eq(%w(issue_4 issue_3 issue_5 issue_2 issue_1))
end
end
end
@@ -68,16 +68,16 @@ feature 'Issue prioritization', feature: true do
issue_6.labels << label_5 # 8 - No priority
login_as user
- visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+ visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
- expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+ expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8')
expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7')
- expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6'])
+ expect(issue_titles[5..-1]).to eq(%w(issue_2 issue_4 issue_6))
end
end
end
diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb
new file mode 100644
index 00000000000..b1a3af612a1
--- /dev/null
+++ b/spec/features/projects/main/rss_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Project RSS' do
+ let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:path) { namespace_project_path(project.namespace, project) }
+
+ context 'when signed in' do
+ before do
+ user = create(:user)
+ project.team << [user, :developer]
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index d6ebb523f95..c7a32a65e49 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -85,7 +85,7 @@ feature 'Projects > Members > Sorting', feature: true do
end
def visit_members_list(sort:)
- visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+ visit namespace_project_project_members_path(project.namespace.to_param, project, sort: sort)
end
def first_member
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0b4dcaa39c6..b64c15e0adc 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -57,6 +57,12 @@ feature 'Projects > Members > User requests access', feature: true do
end
def open_project_settings_menu
- find('#project-settings-button').click
+ page.within('.layout-nav .nav-links') do
+ click_link('Settings')
+ end
+
+ page.within('.page-with-layout-nav .sub-nav') do
+ click_link('Members')
+ end
end
end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
new file mode 100644
index 00000000000..df229d0aa78
--- /dev/null
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+feature 'Project milestone', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) }
+ let(:milestone) { create(:milestone, project: project) }
+
+ before do
+ login_as(user)
+ end
+
+ context 'when project has enabled issues' do
+ before do
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ it 'shows issues tab' do
+ within('#content-body') do
+ expect(page).to have_link 'Issues', href: '#tab-issues'
+ expect(page).to have_selector '.nav-links li.active', count: 1
+ expect(find('.nav-links li.active')).to have_content 'Issues'
+ end
+ end
+
+ it 'shows issues stats' do
+ expect(page).to have_content 'issues:'
+ end
+
+ it 'shows Browse Issues button' do
+ within('#content-body') do
+ expect(page).to have_link 'Browse Issues'
+ end
+ end
+ end
+
+ context 'when project has disabled issues' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ it 'hides issues tab' do
+ within('#content-body') do
+ expect(page).not_to have_link 'Issues', href: '#tab-issues'
+ expect(page).to have_selector '.nav-links li.active', count: 1
+ expect(find('.nav-links li.active')).to have_content 'Merge Requests'
+ end
+ end
+
+ it 'hides issues stats' do
+ expect(page).to have_no_content 'issues:'
+ end
+
+ it 'hides Browse Issues button' do
+ within('#content-body') do
+ expect(page).not_to have_link 'Browse Issues'
+ end
+ end
+
+ it 'does not show an informative message' do
+ expect(page).not_to have_content('Assign some issues to this milestone.')
+ end
+ end
+end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 45185f2dd1f..52196ce49bd 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -16,6 +16,15 @@ feature "New project", feature: true do
expect(find_field("project_visibility_level_#{level}")).to be_checked
end
+
+ it 'saves visibility level on validation error' do
+ visit new_project_path
+
+ choose(key)
+ click_button('Create project')
+
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 8d1214dedb4..162056671e0 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -26,22 +26,66 @@ describe 'Pipelines', :feature, :js do
)
end
- [:all, :running, :branches].each do |scope|
- context "when displaying #{scope}" do
- before do
- visit_project_pipelines(scope: scope)
- end
+ context 'scope' do
+ before do
+ create(:ci_empty_pipeline, status: 'pending', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'running', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'created', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
+ end
- it 'contains pipeline commit short SHA' do
- expect(page).to have_content(pipeline.short_sha)
- end
+ [:all, :running, :pending, :finished, :branches].each do |scope|
+ context "when displaying #{scope}" do
+ before do
+ visit_project_pipelines(scope: scope)
+ end
- it 'contains branch name' do
- expect(page).to have_content(pipeline.ref)
+ it 'contains pipeline commit short SHA' do
+ expect(page).to have_content(pipeline.short_sha)
+ end
+
+ it 'contains branch name' do
+ expect(page).to have_content(pipeline.ref)
+ end
end
end
end
+ context 'header tabs' do
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'shows a tab for All pipelines and count' do
+ expect(page.find('.js-pipelines-tab-all a').text).to include('All')
+ expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
+ end
+
+ it 'shows a tab for Pending pipelines and count' do
+ expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending')
+ expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
+ end
+
+ it 'shows a tab for Running pipelines and count' do
+ expect(page.find('.js-pipelines-tab-running a').text).to include('Running')
+ expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
+ end
+
+ it 'shows a tab for Finished pipelines and count' do
+ expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished')
+ expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
+ end
+
+ it 'shows a tab for Branches' do
+ expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches')
+ end
+
+ it 'shows a tab for Tags' do
+ expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags')
+ end
+ end
+
context 'when pipeline is cancelable' do
let!(:build) do
create(:ci_build, pipeline: pipeline,
@@ -55,15 +99,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be canceled' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-running')
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before do
+ find('.js-pipelines-cancel-button').click
+ wait_for_vue_resource
+ end
it 'indicated that pipelines was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -82,15 +129,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be retried' do
- expect(page).to have_link('Retry')
+ expect(page).to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-failed')
end
context 'when retrying' do
- before { click_link('Retry') }
+ before do
+ find('.js-pipelines-retry-button').click
+ wait_for_vue_resource
+ end
it 'shows running pipeline that is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-running')
end
end
@@ -132,17 +182,17 @@ describe 'Pipelines', :feature, :js do
it 'has link to the manual action' do
find('.js-pipeline-dropdown-manual-actions').click
- expect(page).to have_link('manual build')
+ expect(page).to have_button('manual build')
end
context 'when manual action was played' do
before do
find('.js-pipeline-dropdown-manual-actions').click
- click_link('manual build')
+ click_button('manual build')
end
it 'enqueues manual action job' do
- expect(manual.reload).to be_pending
+ expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
end
end
end
@@ -159,7 +209,7 @@ describe 'Pipelines', :feature, :js do
before { visit_project_pipelines }
it 'is cancelable' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
end
it 'has pipeline running' do
@@ -167,10 +217,10 @@ describe 'Pipelines', :feature, :js do
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before { find('.js-pipelines-cancel-button').trigger('click') }
it 'indicates that pipeline was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -189,7 +239,7 @@ describe 'Pipelines', :feature, :js do
end
it 'is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
end
it 'has failed pipeline' do
@@ -284,6 +334,18 @@ describe 'Pipelines', :feature, :js do
expect(build.reload).to be_canceled
end
end
+
+ context 'dropdown jobs list' do
+ it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
+
+ find('.mini-pipeline-graph-dropdown-item').trigger('click')
+
+ expect(page).to have_selector('.js-ci-action-icon')
+ end
+ end
end
context 'with pagination' do
@@ -315,8 +377,14 @@ describe 'Pipelines', :feature, :js do
visit new_namespace_project_pipeline_path(project.namespace, project)
end
- context 'for valid commit' do
- before { fill_in('pipeline[ref]', with: 'master') }
+ context 'for valid commit', js: true do
+ before do
+ click_button project.default_branch
+
+ page.within '.dropdown-menu' do
+ click_link 'master'
+ end
+ end
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
@@ -333,15 +401,6 @@ describe 'Pipelines', :feature, :js do
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
-
- context 'for invalid commit' do
- before do
- fill_in('pipeline[ref]', with: 'invalid-reference')
- click_on 'Create pipeline'
- end
-
- it { expect(page).to have_content('Reference not found') }
- end
end
describe 'Create pipelines' do
@@ -353,18 +412,22 @@ describe 'Pipelines', :feature, :js do
describe 'new pipeline page' do
it 'has field to add a new pipeline' do
- expect(page).to have_field('pipeline[ref]')
+ expect(page).to have_selector('.js-branch-select')
+ expect(find('.js-branch-select')).to have_content project.default_branch
expect(page).to have_content('Create for')
end
end
describe 'find pipelines' do
it 'shows filtered pipelines', js: true do
- fill_in('pipeline[ref]', with: 'fix')
- find('input#ref').native.send_keys(:keydown)
+ click_button project.default_branch
+
+ page.within '.dropdown-menu' do
+ find('.dropdown-input-field').native.send_keys('fix')
- within('.ui-autocomplete') do
- expect(page).to have_selector('li', text: 'fix')
+ page.within '.dropdown-content' do
+ expect(page).to have_content('fix')
+ end
end
end
end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f5adb53a2dc..24d22a092d4 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Setup Mattermost slash commands', feature: true do
+feature 'Setup Mattermost slash commands', :feature, :js do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
@@ -62,11 +62,11 @@ feature 'Setup Mattermost slash commands', feature: true do
click_link 'Add to Mattermost'
- team_name = teams.first[1]['display_name']
- select_element = find('select#mattermost_team_id')
+ team_name = teams.first['display_name']
+ select_element = find('#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to eq('disabled')
+ expect(select_element['disabled']).to be(true)
expect(selected_option).to have_content(team_name.to_s)
end
@@ -75,7 +75,7 @@ feature 'Setup Mattermost slash commands', feature: true do
click_link 'Add to Mattermost'
- expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s)
+ expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id'])
end
it 'shows an explanation user is a member of multiple teams' do
@@ -92,12 +92,9 @@ feature 'Setup Mattermost slash commands', feature: true do
click_link 'Add to Mattermost'
- select_element = find('select#mattermost_team_id')
- selected_option = select_element.find('option[selected]')
+ select_element = find('#mattermost_team_id')
- expect(select_element['disabled']).to be(nil)
- expect(selected_option).to have_content('Select team...')
- # The 'Select team...' placeholder is item `0`.
+ expect(select_element['disabled']).to be(false)
expect(select_element.all('option').count).to eq(3)
end
@@ -110,20 +107,37 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(page).to have_content('test mattermost error message')
end
+ it 'enables the submit button if the required fields are provided', :js do
+ stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ expect(find('input[type="submit"]')['disabled']).not_to be(true)
+ end
+
+ it 'disables the submit button if the required fields are not provided', :js do
+ stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ fill_in('mattermost_trigger', with: '')
+
+ expect(find('input[type="submit"]')['disabled']).to be(true)
+ end
+
def stub_teams(count: 0)
teams = create_teams(count)
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams }
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
teams
end
def create_teams(count = 0)
- teams = {}
+ teams = []
count.times do |i|
- i += 1
- teams[i] = { id: i, display_name: i }
+ teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" })
end
teams
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98..c0a4a1e4bf5 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
background do
service.fields
- service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+ service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
project.team << [user, :master]
login_as(user)
end
@@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
expect(page.find_field("service_merge_request_channel").value).to have_content '3'
expect(page.find_field("service_note_channel").value).to have_content '4'
expect(page.find_field("service_tag_push_channel").value).to have_content '5'
- expect(page.find_field("service_build_channel").value).to have_content '6'
+ expect(page.find_field("service_pipeline_channel").value).to have_content '6'
expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 6815039d5ed..321af416c91 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -62,4 +62,27 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
+
+ describe 'Checkbox to enable merge request link' do
+ before do
+ visit edit_project_path(project)
+ end
+
+ scenario 'is initially checked' do
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).to be_checked
+ end
+
+ scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do
+ uncheck('project_printing_merge_request_link_enabled')
+ click_on('Save')
+
+ # Wait for save to complete and page to reload
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.printing_merge_request_link_enabled).to be(false)
+ end
+ end
end
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
new file mode 100644
index 00000000000..9ac51997d65
--- /dev/null
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Project Tree RSS' do
+ let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:path) { namespace_project_tree_path(project.namespace, project, :master) }
+
+ context 'when signed in' do
+ before do
+ user = create(:user)
+ project.team << [user, :developer]
+ login_as(user)
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "an autodiscoverable RSS feed without a private token"
+ end
+end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
new file mode 100644
index 00000000000..6825b95c8aa
--- /dev/null
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views Git access wiki page', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:wiki_page) do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ before do
+ login_as(user)
+ end
+
+ scenario 'Visit Wiki Page Current Commit' do
+ visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+ click_link 'Clone repository'
+ expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}")
+ expect(page).to have_text(project.wiki.http_url_to_repo(user))
+ end
+end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa96..aedc0333cb9 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
context 'in the user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
- scenario 'the home page' do
- click_link 'Edit'
-
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Save changes'
-
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ context 'the home page' do
+ scenario 'success when the wiki content is not empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Save changes'
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'failure when the wiki content is empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: ''
+ click_button 'Save changes'
+
+ expect(page).to have_selector('.wiki-form')
+ expect(page).to have_content('Edit Page')
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content('Content can\'t be blank')
+ expect(find('textarea#wiki_content').value).to eq ''
+ end
end
end
diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
new file mode 100644
index 00000000000..c17e06612de
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Projects > Wiki > User views the wiki page', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:old_page_version_id) { wiki_page.versions.last.id }
+ let(:wiki_page) do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+ WikiPages::UpdateService.new(
+ project,
+ user,
+ message: 'updated home',
+ content: 'updated [some link](other-page)',
+ format: :markdown
+ ).execute(wiki_page)
+ end
+
+ scenario 'Visit Wiki Page Current Commit' do
+ visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+ expect(page).to have_selector('a.btn', text: 'Edit')
+ end
+
+ scenario 'Visit Wiki Page Historical Commit' do
+ visit namespace_project_wiki_path(
+ project.namespace,
+ project,
+ wiki_page,
+ version_id: old_page_version_id
+ )
+
+ expect(page).not_to have_selector('a.btn', text: 'Edit')
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c30d38b6508..ba56030e28d 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -18,7 +18,7 @@ feature 'Project', feature: true do
it 'passes through html-pipeline' do
project.update_attribute(:description, 'This project is the :poop:')
visit path
- expect(page).to have_css('.project-home-desc > p > img')
+ expect(page).to have_css('.project-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
@@ -56,7 +56,7 @@ feature 'Project', feature: true do
end
describe 'removal', js: true do
- let(:user) { create(:user) }
+ let(:user) { create(:user, username: 'test', name: 'test') }
let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
before do
@@ -67,7 +67,7 @@ feature 'Project', feature: true do
it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
- expect(page).to have_content "Project 'project1' will be deleted."
+ expect(page).to have_content "Project 'test / project1' will be deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
expect(project.merge_requests).to be_empty
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 7da05defa81..a6560a81096 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe "Search", feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let(:user) { create(:user) }
@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.filtered-search')
- expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her issues page when issues authored is clicked' do
@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.filtered-search')
- expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her MR page when MR assigned is clicked' do
@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her MR page when MR authored is clicked' do
@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 4e7a2c0ecc0..350de2e5b6b 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -110,6 +110,20 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index c74cdc05593..62364206440 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -110,6 +110,20 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 485ef335b78..0e0c3140fd0 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -110,6 +110,20 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 0f30f562539..ccfafe6db7d 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,16 +10,12 @@ feature 'Master deletes tag', feature: true do
visit namespace_project_tags_path(project.namespace, project)
end
- context 'from the tags list page' do
+ context 'from the tags list page', js: true do
scenario 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
- page.within('.content') do
- first('.btn-remove').click
- end
+ delete_first_tag
- expect(current_path).to eq(
- namespace_project_tags_path(project.namespace, project))
expect(page).not_to have_content 'v1.1.0'
end
end
@@ -37,4 +33,23 @@ feature 'Master deletes tag', feature: true do
expect(page).not_to have_content 'v1.0.0'
end
end
+
+ context 'when pre-receive hook fails', js: true do
+ before do
+ allow_any_instance_of(GitHooksService).to receive(:execute)
+ .and_raise(GitHooksService::PreReceiveError, 'Do not delete tags')
+ end
+
+ scenario 'shows the error message' do
+ delete_first_tag
+
+ expect(page).to have_content('Do not delete tags')
+ end
+ end
+
+ def delete_first_tag
+ page.within('.content') do
+ first('.btn-remove').click
+ end
+ end
end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 29d2c244720..555f84c4772 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -27,10 +27,20 @@ feature 'Master views tags', feature: true do
context 'when project has tags' do
let(:project) { create(:project, namespace: user.namespace) }
+ let(:repository) { project.repository }
+
before do
visit namespace_project_tags_path(project.namespace, project)
end
+ scenario 'avoids a N+1 query in branches index' do
+ control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_tags_path(project.namespace, project) }.count
+
+ %w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') }
+
+ expect { visit namespace_project_tags_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+ end
+
scenario 'views the tags list page' do
expect(page).to have_content 'v1.0.0'
end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
index fec28c55d30..4d5bd476301 100644
--- a/spec/features/todos/todos_sorting_spec.rb
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -56,8 +56,8 @@ describe "Dashboard > User sorts todos", feature: true do
expect(results_list.all('p')[4]).to have_content("merge_request_1")
end
- it "sorts by priority" do
- click_link "Priority"
+ it "sorts by label priority" do
+ click_link "Label priority"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_3")
@@ -85,8 +85,8 @@ describe "Dashboard > User sorts todos", feature: true do
visit dashboard_todos_path
end
- it "doesn't mix issues and merge requests priorities" do
- click_link "Priority"
+ it "doesn't mix issues and merge requests label priorities" do
+ click_link "Label priority"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_1")
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3495091a0d5..850020109d4 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -31,14 +31,16 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows due date as today' do
- page.within first('.todo') do
+ within first('.todo') do
expect(page).to have_content 'Due today'
end
end
shared_examples 'deleting the todo' do
before do
- first('.js-done-todo').click
+ within first('.todo') do
+ click_link 'Done'
+ end
end
it 'is marked as done-reversible in the list' do
@@ -62,9 +64,11 @@ describe 'Dashboard Todos', feature: true do
shared_examples 'deleting and restoring the todo' do
before do
- first('.js-done-todo').click
- wait_for_ajax
- first('.js-undo-todo').click
+ within first('.todo') do
+ click_link 'Done'
+ wait_for_ajax
+ click_link 'Undo'
+ end
end
it 'is marked back as pending in the list' do
@@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User has done todos', js: true do
+ before do
+ create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
+ login_as(user)
+ visit dashboard_todos_path(state: :done)
+ end
+
+ it 'has the done todo present' do
+ expect(page).to have_selector('.todos-list .todo.todo-done', count: 1)
+ end
+
+ describe 'restoring the todo' do
+ before do
+ within first('.todo') do
+ click_link 'Add todo'
+ end
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo.todo-done')
+ end
+
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Done 0'
+ end
+ end
+ end
+
context 'User has Todos with labels spanning multiple projects' do
before do
label1 = create(:label, project: project)
@@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
- click_link('Mark all as done')
+ click_link 'Mark all as done'
end
it 'shows "All done" message!' do
@@ -151,6 +184,60 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
+
+ it 'shows "Undo mark all as done" button' do
+ expect(page).to have_selector('.js-todos-mark-all', visible: false)
+ expect(page).to have_selector('.js-todos-undo-all', visible: true)
+ end
+ end
+
+ describe 'undo mark all as done', js: true do
+ before do
+ visit dashboard_todos_path
+ end
+
+ it 'shows the restored todo list' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ expect(page).to have_selector('.gl-pagination')
+ expect(page).not_to have_content "You're all done!"
+ end
+
+ it 'updates todo count' do
+ mark_all_and_undo
+
+ expect(page).to have_content 'To do 2'
+ expect(page).to have_content 'Done 0'
+ end
+
+ it 'shows "Mark all as done" button' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.js-todos-mark-all', visible: true)
+ expect(page).to have_selector('.js-todos-undo-all', visible: false)
+ end
+
+ context 'User has deleted a todo' do
+ before do
+ within first('.todo') do
+ click_link 'Done'
+ end
+ end
+
+ it 'shows the restored todo list with the deleted todo' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1)
+ end
+ end
+
+ def mark_all_and_undo
+ click_link 'Mark all as done'
+ wait_for_ajax
+ click_link 'Undo mark all as done'
+ wait_for_ajax
+ end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 4a7511589d6..c1ae6db00c6 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,28 +1,175 @@
require 'spec_helper'
-describe 'Triggers' do
+feature 'Triggers', feature: true, js: true do
+ let(:trigger_title) { 'trigger desc' }
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:guest_user) { create(:user) }
before { login_as(user) }
before do
- @project = FactoryGirl.create :empty_project
+ @project = create(:empty_project)
@project.team << [user, :master]
+ @project.team << [user2, :master]
+ @project.team << [guest_user, :guest]
visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
- context 'create a trigger' do
- before do
- click_on 'Add trigger'
- expect(@project.triggers.count).to eq(1)
+ describe 'create trigger workflow' do
+ scenario 'prevents adding new trigger with no description' do
+ fill_in 'trigger_description', with: ''
+ click_button 'Add trigger'
+
+ # See if input has error due to empty value
+ expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;'
+ end
+
+ scenario 'adds new trigger with description' do
+ fill_in 'trigger_description', with: 'trigger desc'
+ click_button 'Add trigger'
+
+ # See if "trigger creation successful" message displayed and description and owner are correct
+ expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
+ expect(page.find('.triggers-list')).to have_content 'trigger desc'
+ expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+ end
+ end
+
+ describe 'edit trigger workflow' do
+ let(:new_trigger_title) { 'new trigger' }
+
+ scenario 'click on edit trigger opens edit trigger page' do
+ create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if edit page has correct descrption
+ find('a[title="Edit"]').click
+ expect(page.find('#trigger_description').value).to have_content 'trigger desc'
+ end
+
+ scenario 'edit trigger and save' do
+ create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if edit page opens, then fill in new description and save
+ find('a[title="Edit"]').click
+ fill_in 'trigger_description', with: new_trigger_title
+ click_button 'Save trigger'
+
+ # See if "trigger updated successfully" message displayed and description and owner are correct
+ expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+ expect(page.find('.triggers-list')).to have_content new_trigger_title
+ expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+ end
+
+ scenario 'edit "legacy" trigger and save' do
+ # Create new trigger without owner association, i.e. Legacy trigger
+ create(:ci_trigger, owner: nil, project: @project)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if the trigger can be edited and description is blank
+ find('a[title="Edit"]').click
+ expect(page.find('#trigger_description').value).to have_content ''
+
+ # See if trigger can be updated with description and saved successfully
+ fill_in 'trigger_description', with: new_trigger_title
+ click_button 'Save trigger'
+ expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+ expect(page.find('.triggers-list')).to have_content new_trigger_title
+ end
+ end
+
+ describe 'trigger "Take ownership" workflow' do
+ before(:each) do
+ create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+ end
+
+ scenario 'button "Take ownership" has correct alert' do
+ expected_alert = 'By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?'
+ expect(page.find('a.btn-trigger-take-ownership')['data-confirm']).to eq expected_alert
end
- it 'contains trigger token' do
- expect(page).to have_content(@project.triggers.first.token)
+ scenario 'take trigger ownership' do
+ # See if "Take ownership" on trigger works post trigger creation
+ find('a.btn-trigger-take-ownership').click
+ page.accept_confirm do
+ expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
+ expect(page.find('.triggers-list')).to have_content trigger_title
+ expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+ end
end
+ end
+
+ describe 'trigger "Revoke" workflow' do
+ before(:each) do
+ create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+ end
+
+ scenario 'button "Revoke" has correct alert' do
+ expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
+ expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert
+ end
+
+ scenario 'revoke trigger' do
+ # See if "Revoke" on trigger works post trigger creation
+ find('a.btn-trigger-revoke').click
+ page.accept_confirm do
+ expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
+ end
+ end
+ end
+
+ describe 'show triggers workflow' do
+ scenario 'contains trigger description placeholder' do
+ expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
+ end
+
+ scenario 'show "legacy" badge for legacy trigger' do
+ create(:ci_trigger, owner: nil, project: @project)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable
+ expect(page.find('.triggers-list')).to have_content 'legacy'
+ expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
+ end
+
+ scenario 'show "invalid" badge for trigger with owner having insufficient permissions' do
+ create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable
+ expect(page.find('.triggers-list')).to have_content 'invalid'
+ expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
+ end
+
+ scenario 'do not show "Edit" or full token for not owned trigger' do
+ # Create trigger with user different from current_user
+ create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button
+ expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
+ expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard')
+
+ # See if trigger owner name doesn't match with current_user and trigger is non-editable
+ expect(page.find('.triggers-list .trigger-owner')).not_to have_content @user.name
+ expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
+ end
+
+ scenario 'show "Edit" and full token for owned trigger' do
+ create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
+
+ # See if trigger shows full token and has copy-to-clipboard button
+ expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
+ expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard')
- it 'revokes the trigger' do
- click_on 'Revoke'
- expect(@project.triggers.count).to eq(0)
+ # See if trigger owner name matches with current_user and is editable
+ expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name
+ expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
end
end
end
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
new file mode 100644
index 00000000000..f88a515f7fc
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to group', feature: true do
+ scenario 'they see the new avatar' do
+ user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+ login_as(user)
+
+ visit edit_group_path(group)
+ attach_file(
+ 'group_avatar',
+ Rails.root.join('spec', 'fixtures', 'dk.png'),
+ visible: false
+ )
+
+ click_button 'Save group'
+
+ visit group_path(group)
+
+ expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"]))
+
+ # Cheating here to verify something that isn't user-facing, but is important
+ expect(group.reload.avatar.file).to exist
+ end
+end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
new file mode 100644
index 00000000000..0dfd29045e5
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to profile', feature: true do
+ scenario 'they see their new avatar' do
+ user = create(:user)
+ login_as(user)
+
+ visit profile_path
+ attach_file(
+ 'user_avatar',
+ Rails.root.join('spec', 'fixtures', 'dk.png'),
+ visible: false
+ )
+
+ click_button 'Update profile settings'
+
+ visit user_path(user)
+
+ expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"]))
+
+ # Cheating here to verify something that isn't user-facing, but is important
+ expect(user.reload.avatar.file).to exist
+ end
+end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
new file mode 100644
index 00000000000..0c160dd74b4
--- /dev/null
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+feature 'User uploads file to note', feature: true do
+ include DropzoneHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
+
+ scenario 'they see the attached file', js: true do
+ issue = create(:issue, project: project, author: user)
+
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Comment'
+ wait_for_ajax
+
+ expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+ .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+ end
+end
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
new file mode 100644
index 00000000000..336c4092c98
--- /dev/null
+++ b/spec/features/user_callout_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'User Callouts', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ end
+
+ it 'takes you to the profile preferences when the link is clicked' do
+ visit dashboard_projects_path
+ click_link 'Check it out'
+ expect(current_path).to eq profile_preferences_path
+ end
+
+ describe 'user callout should appear in two routes' do
+ it 'shows up on the user profile' do
+ visit user_path(user)
+ expect(find('.user-callout')).to have_content 'Customize your experience'
+ end
+
+ it 'shows up on the dashboard projects' do
+ visit dashboard_projects_path
+ expect(find('.user-callout')).to have_content 'Customize your experience'
+ end
+ end
+
+ it 'hides the user callout when click on the dismiss icon' do
+ visit user_path(user)
+ within('.user-callout') do
+ find('.close-user-callout').click
+ end
+ expect(page).not_to have_selector('#user-callout')
+ end
+end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
new file mode 100644
index 00000000000..14564abb16d
--- /dev/null
+++ b/spec/features/users/rss_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+feature 'User RSS' do
+ let(:path) { user_path(create(:user)) }
+
+ context 'when signed in' do
+ before do
+ login_as(create(:user))
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button with current_user's private token"
+ end
+
+ context 'when signed out' do
+ before do
+ visit path
+ end
+
+ it_behaves_like "it has an RSS button without a private token"
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 2a008427478..ee52dc65175 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -101,6 +101,41 @@ describe IssuesFinder do
end
end
+ context 'filtering by started milestone' do
+ let(:params) { { milestone_title: Milestone::Started.name } }
+
+ let(:project_no_started_milestones) { create(:empty_project, :public) }
+ let(:project_started_1_and_2) { create(:empty_project, :public) }
+ let(:project_started_8) { create(:empty_project, :public) }
+
+ let(:yesterday) { Date.today - 1.day }
+ let(:tomorrow) { Date.today + 1.day }
+ let(:two_days_ago) { Date.today - 2.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
+ create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
+ create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
+ create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+ create(:milestone, project: project_started_8, title: '7.0'),
+ create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
+ create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
+ ]
+ end
+
+ before do
+ milestones.each do |milestone|
+ create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+ end
+ end
+
+ it 'returns issues in the started milestones for each project' do
+ expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0')
+ expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday)
+ end
+ end
+
context 'filtering by label' do
let(:params) { { label_name: label.title } }
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
new file mode 100644
index 00000000000..cf691cf684b
--- /dev/null
+++ b/spec/finders/members_finder_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe MembersFinder, '#execute' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, :access_requestable, parent: group) }
+ let(:project) { create(:project, namespace: nested_group) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:user4) { create(:user) }
+
+ it 'returns members for project and parent groups' do
+ nested_group.request_access(user1)
+ member1 = group.add_master(user2)
+ member2 = nested_group.add_master(user3)
+ member3 = project.add_master(user4)
+
+ result = described_class.new(project, user2).execute
+
+ expect(result.to_a).to eq([member3, member2, member1])
+ end
+end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index f8b05d4e9bc..77a04507be1 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -111,7 +111,7 @@ describe NotesFinder do
end
it 'raises an exception for an invalid target_type' do
- params.merge!(target_type: 'invalid')
+ params[:target_type] = 'invalid'
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
end
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
new file mode 100644
index 00000000000..fd92664ca24
--- /dev/null
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe PersonalAccessTokensFinder do
+ def finder(options = {})
+ described_class.new(options)
+ end
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:params) { {} }
+ let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
+ let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+ let!(:active_impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) }
+ let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) }
+
+ subject { finder(params).execute }
+
+ describe 'without user' do
+ it do
+ is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
+ revoked_personal_access_token, expired_personal_access_token,
+ revoked_impersonation_token, expired_impersonation_token)
+ end
+
+ describe 'without impersonation' do
+ before { params[:impersonation] = false }
+
+ it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) }
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_personal_access_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
+ end
+ end
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_impersonation_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+ end
+ end
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it do
+ is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
+ expired_impersonation_token, revoked_impersonation_token)
+ end
+ end
+
+ describe 'with id' do
+ subject { finder(params).find_by(id: active_personal_access_token.id) }
+
+ it { is_expected.to eq(active_personal_access_token) }
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe 'with token' do
+ subject { finder(params).find_by(token: active_personal_access_token.token) }
+
+ it { is_expected.to eq(active_personal_access_token) }
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe 'with user' do
+ let(:user2) { create(:user) }
+ let!(:other_user_active_personal_access_token) { create(:personal_access_token, user: user2) }
+ let!(:other_user_expired_personal_access_token) { create(:personal_access_token, :expired, user: user2) }
+ let!(:other_user_revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user2) }
+ let!(:other_user_active_impersonation_token) { create(:personal_access_token, :impersonation, user: user2) }
+ let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) }
+ let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) }
+
+ before { params[:user] = user }
+
+ it do
+ is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
+ revoked_personal_access_token, expired_personal_access_token,
+ revoked_impersonation_token, expired_impersonation_token)
+ end
+
+ describe 'without impersonation' do
+ before { params[:impersonation] = false }
+
+ it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) }
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_personal_access_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
+ end
+ end
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_impersonation_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+ end
+ end
+
+ describe 'with active state' do
+ before { params[:state] = 'active' }
+
+ it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) }
+ end
+
+ describe 'with inactive state' do
+ before { params[:state] = 'inactive' }
+
+ it do
+ is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
+ expired_impersonation_token, revoked_impersonation_token)
+ end
+ end
+
+ describe 'with id' do
+ subject { finder(params).find_by(id: active_personal_access_token.id) }
+
+ it { is_expected.to eq(active_personal_access_token) }
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe 'with token' do
+ subject { finder(params).find_by(token: active_personal_access_token.token) }
+
+ it { is_expected.to eq(active_personal_access_token) }
+
+ describe 'with impersonation' do
+ before { params[:impersonation] = true }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index fdc8215aa47..6bada7b3eb9 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -39,8 +39,8 @@ describe PipelinesFinder do
end
end
- # Scoping to running will speed up the test as it doesn't hit the FS
- let(:params) { { scope: 'running' } }
+ # Scoping to pending will speed up the test as it doesn't hit the FS
+ let(:params) { { scope: 'pending' } }
it 'orders in descending order on ID' do
feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 8e19cee5440..21c078e0f44 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -11,6 +11,7 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
+ "relative_position": { "type": "integer" },
"labels": {
"type": "array",
"items": {
diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json
new file mode 100644
index 00000000000..f2ee9c925ae
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v3/issues.json
@@ -0,0 +1,77 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "milestone": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "user_notes_count": { "type": "integer" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "due_date": { "type": ["date", "null"] },
+ "confidential": { "type": "boolean" },
+ "web_url": { "type": "uri" },
+ "subscribed": { "type": ["boolean"] }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "labels",
+ "milestone", "assignee", "author", "user_notes_count",
+ "upvotes", "downvotes", "due_date", "confidential",
+ "web_url", "subscribed"
+ ],
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
new file mode 100644
index 00000000000..01f9fbb2c89
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json
@@ -0,0 +1,89 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "target_branch": { "type": "string" },
+ "source_branch": { "type": "string" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "source_project_id": { "type": "integer" },
+ "target_project_id": { "type": "integer" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "work_in_progress": { "type": "boolean" },
+ "milestone": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "merge_when_build_succeeds": { "type": "boolean" },
+ "merge_status": { "type": "string" },
+ "sha": { "type": "string" },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "user_notes_count": { "type": "integer" },
+ "should_remove_source_branch": { "type": ["boolean", "null"] },
+ "force_remove_source_branch": { "type": ["boolean", "null"] },
+ "web_url": { "type": "uri" },
+ "subscribed": { "type": ["boolean"] }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "target_branch",
+ "source_branch", "upvotes", "downvotes", "author",
+ "assignee", "source_project_id", "target_project_id",
+ "labels", "work_in_progress", "milestone", "merge_when_build_succeeds",
+ "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+ "should_remove_source_branch", "force_remove_source_branch",
+ "web_url", "subscribed"
+ ],
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
new file mode 100644
index 00000000000..52199e75734
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -0,0 +1,76 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "milestone": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "user_notes_count": { "type": "integer" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "due_date": { "type": ["date", "null"] },
+ "confidential": { "type": "boolean" },
+ "web_url": { "type": "uri" }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "labels",
+ "milestone", "assignee", "author", "user_notes_count",
+ "upvotes", "downvotes", "due_date", "confidential",
+ "web_url"
+ ],
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
new file mode 100644
index 00000000000..51642e8cbb8
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -0,0 +1,88 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "target_branch": { "type": "string" },
+ "source_branch": { "type": "string" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "source_project_id": { "type": "integer" },
+ "target_project_id": { "type": "integer" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "work_in_progress": { "type": "boolean" },
+ "milestone": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "merge_when_pipeline_succeeds": { "type": "boolean" },
+ "merge_status": { "type": "string" },
+ "sha": { "type": "string" },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "user_notes_count": { "type": "integer" },
+ "should_remove_source_branch": { "type": ["boolean", "null"] },
+ "force_remove_source_branch": { "type": ["boolean", "null"] },
+ "web_url": { "type": "uri" }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "target_branch",
+ "source_branch", "upvotes", "downvotes", "author",
+ "assignee", "source_project_id", "target_project_id",
+ "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
+ "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+ "should_remove_source_branch", "force_remove_source_branch",
+ "web_url"
+ ],
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index 6181b3ccc86..6181b3ccc86 100644
--- a/spec/fixtures/api/schemas/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
index 5587cfec61a..5587cfec61a 100644
--- a/spec/fixtures/api/schemas/user/public.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index f3e7c2d1a9f..0cdbc32431d 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -79,6 +79,11 @@ As permissive as it is, we've allowed even more stuff:
<span>span tag</span>
+<details>
+<summary>Summary lines are collapsible:</summary>
+Hiding the details until expanded.
+</details>
+
<a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a>
<a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a>
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index fd40fe99941..5c07ea8a872 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -58,7 +58,7 @@ describe ApplicationHelper do
project = create(:empty_project, avatar: File.open(uploaded_image_temp_path))
avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
- expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
+ expect(helper.project_icon(project.full_path).to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
@@ -68,7 +68,7 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
- expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match(
+ expect(helper.project_icon(project.full_path).to_s).to match(
image_tag(avatar_url))
end
end
@@ -193,8 +193,8 @@ describe ApplicationHelper do
describe 'time_ago_with_tooltip' do
def element(*arguments)
Time.zone = 'UTC'
- time = Time.zone.parse('2015-07-02 08:23')
- element = helper.time_ago_with_tooltip(time, *arguments)
+ @time = Time.zone.parse('2015-07-02 08:23')
+ element = helper.time_ago_with_tooltip(@time, *arguments)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
end
@@ -204,7 +204,7 @@ describe ApplicationHelper do
end
it 'includes the date string' do
- expect(element.text).to eq '2015-07-02 08:23:00 UTC'
+ expect(element.text).to eq @time.strftime("%b %d, %Y")
end
it 'has a datetime attribute' do
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 49ea4fa6d3e..cd3281d6f51 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -55,7 +55,7 @@ describe AuthHelper do
context 'all the button based providers are disabled via application_setting' do
it 'returns false' do
stub_application_setting(
- disabled_oauth_sign_in_sources: ['github', 'twitter']
+ disabled_oauth_sign_in_sources: %w(github twitter)
)
expect(helper.button_based_providers_enabled?).to be false
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index fa516f9903e..bead7948486 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -19,12 +19,12 @@ describe BlobHelper do
describe '#highlight' do
it 'returns plaintext for unknown lexer context' do
result = helper.highlight(blob_name, no_context_content)
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
+ expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
end
it 'highlights single block' do
- expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
+ expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end
@@ -43,10 +43,10 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do
- %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
-<span id="LC2" class="line"><span class="gi">+bbb</span></span>
-<span id="LC3" class="line"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line"> ddd</span></code></pre>)
+ %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
end
it 'highlights each line properly' do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 637b02d9388..174cc84a97b 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -16,4 +16,11 @@ describe CiStatusHelper do
helper.ci_icon_for_status(failed_commit.status)
end
end
+
+ describe "#pipeline_status_cache_key" do
+ it "builds a cache key for pipeline status" do
+ pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success")
+ expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
+ end
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 3223556e1d3..cd112dbb2fb 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -43,4 +43,36 @@ describe EmailsHelper do
end
end
end
+
+ describe '#header_logo' do
+ context 'there is a brand item with a logo' do
+ it 'returns the brand header logo' do
+ appearance = create :appearance, header_logo: fixture_file_upload(
+ Rails.root.join('spec/fixtures/dk.png')
+ )
+
+ expect(header_logo).to eq(
+ %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
+ )
+ end
+ end
+
+ context 'there is a brand item without a logo' do
+ it 'returns the default header logo' do
+ create :appearance, header_logo: nil
+
+ expect(header_logo).to eq(
+ %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+ )
+ end
+ end
+
+ context 'there is no brand item' do
+ it 'returns the default header logo' do
+ expect(header_logo).to eq(
+ %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+ )
+ end
+ end
+ end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 594b40303bc..70443d27f33 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -28,7 +28,7 @@ describe EventsHelper do
it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
- expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
+ expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
@@ -55,10 +55,15 @@ describe EventsHelper do
it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \
- "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
- " <span class=\"s1\">\'hello world\'</span>\n" \
- "<span class=\"k\">end</span>\n" \
- '</code></pre>'
+ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
+ "</code></pre>"
+ expect(helper.event_note(input)).to eq(expected)
+ end
+
+ it 'preserves style attribute within a tag' do
+ input = '<span class="" style="background-color: #44ad8e; color: #FFFFFF;"></span>'
+ expected = '<p><span style="background-color: #44ad8e; color: #FFFFFF;"></span></p>'
+
expect(helper.event_note(input)).to eq(expected)
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index b8ec3521edb..6cf3f86680a 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
- to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
end
end
@@ -152,9 +152,8 @@ describe GitlabMarkdownHelper do
end
describe '#first_line_in_markdown' do
- let(:text) { "@#{user.username}, can you look at this?\nHello world\n"}
-
it 'truncates Markdown properly' do
+ text = "@#{user.username}, can you look at this?\nHello world\n"
actual = first_line_in_markdown(text, 100, project: project)
doc = Nokogiri::HTML.parse(actual)
@@ -169,6 +168,23 @@ describe GitlabMarkdownHelper do
expect(doc.content).to eq "@#{user.username}, can you look at this?..."
end
+
+ it 'truncates Markdown with emoji properly' do
+ text = "foo :wink:\nbar :grinning:"
+ actual = first_line_in_markdown(text, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ # But also account for the 2 errors caused by the unknown `gl-emoji` elements
+ expect(doc.errors.length).to eq(2)
+
+ expect(doc.css('gl-emoji').length).to eq(2)
+ expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
+ expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+
+ expect(doc.content).to eq "foo 😉\nbar 😀"
+ end
end
describe '#cross_project_reference' do
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index df71680e44c..93bb711f29a 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -51,7 +51,7 @@ describe IssuablesHelper do
utf8: '✓',
author_id: '11',
assignee_id: '18',
- label_name: ['bug', 'discussion', 'documentation'],
+ label_name: %w(bug discussion documentation),
milestone_title: 'v4.0',
sort: 'due_date_asc',
namespace_id: 'gitlab-org',
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 13fb9c1f1a7..f0554cc068d 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -55,8 +55,8 @@ describe IssuesHelper do
describe "merge_requests_sentence" do
subject { merge_requests_sentence(merge_requests)}
let(:merge_requests) do
- [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
- build(:merge_request, iid: 3)]
+ [build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+ build(:merge_request, iid: 3)]
end
it { is_expected.to eq("!1, !2, or !3") }
@@ -113,7 +113,7 @@ describe IssuesHelper do
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"])
+ expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
end
end
@@ -131,4 +131,36 @@ describe IssuesHelper do
expect(options).to have_selector('option', text: milestone2.title)
end
end
+
+ describe "#link_to_discussions_to_resolve" do
+ describe "passing only a merge request" do
+ let(:merge_request) { create(:merge_request) }
+
+ it "links just the merge request" do
+ expected_path = namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+
+ expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path)
+ end
+
+ it "containst the reference to the merge request" do
+ expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference)
+ end
+ end
+
+ describe "when passing a discussion" do
+ let(:diff_note) { create(:diff_note_on_merge_request) }
+ let(:merge_request) { diff_note.noteable }
+ let(:discussion) { Discussion.new([diff_note]) }
+
+ it "links to the merge request with first note if a single discussion was passed" do
+ expected_path = Gitlab::UrlBuilder.build(diff_note)
+
+ expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path)
+ end
+
+ it "contains both the reference to the merge request and a mention of the discussion" do
+ expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})")
+ end
+ end
+ end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 14a95479339..3cb809d42b5 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -17,7 +17,7 @@ describe MilestonesHelper do
it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") }
it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") }
it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") }
- it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") }
+ it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") }
end
describe '#milestone_counts' do
@@ -47,4 +47,58 @@ describe MilestonesHelper do
end
end
end
+
+ describe '#milestone_remaining_days' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
+ context 'when less than 31 days remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+ it 'returns days remaining' do
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+ end
+ end
+
+ context 'when less than 1 year and more than 30 days remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+ it 'returns months remaining' do
+ expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+ end
+ end
+
+ context 'when more than 1 year remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+ it 'returns years remaining' do
+ expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+ end
+ end
+
+ context 'when milestone is expired' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+ it 'returns "Past due"' do
+ expect(milestone_remaining).to eq("<strong>Past due</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the future' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+ it 'returns "Upcoming"' do
+ expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the past' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+ it 'returns days elapsed' do
+ expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index aca0bb1d794..fc6ad6419ac 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,6 +63,46 @@ describe ProjectsHelper do
end
end
+ describe "#project_list_cache_key" do
+ let(:project) { create(:project) }
+
+ it "includes the namespace" do
+ expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key)
+ end
+
+ it "includes the project" do
+ expect(helper.project_list_cache_key(project)).to include(project.cache_key)
+ end
+
+ it "includes the controller name" do
+ expect(helper.controller).to receive(:controller_name).and_return("testcontroller")
+
+ expect(helper.project_list_cache_key(project)).to include("testcontroller")
+ end
+
+ it "includes the controller action" do
+ expect(helper.controller).to receive(:action_name).and_return("testaction")
+
+ expect(helper.project_list_cache_key(project)).to include("testaction")
+ end
+
+ it "includes the application settings" do
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ expect(helper.project_list_cache_key(project)).to include(settings.cache_key)
+ end
+
+ it "includes a version" do
+ expect(helper.project_list_cache_key(project)).to include("v2.3")
+ end
+
+ it "includes the pipeline status when there is a status" do
+ create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+
+ expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success")
+ end
+ end
+
describe 'link_to_member' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
new file mode 100644
index 00000000000..f3f174f3d14
--- /dev/null
+++ b/spec/helpers/rss_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe RssHelper do
+ describe '#rss_url_options' do
+ context 'when signed in' do
+ it "includes the current_user's private_token" do
+ current_user = create(:user)
+ allow(helper).to receive(:current_user).and_return(current_user)
+ expect(helper.rss_url_options).to include private_token: current_user.private_token
+ end
+ end
+
+ context 'when signed out' do
+ it "does not have a private_token" do
+ allow(helper).to receive(:current_user).and_return(nil)
+ expect(helper.rss_url_options[:private_token]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 4da1569e59f..28b8def331d 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -20,97 +20,97 @@ describe SubmoduleHelper do
it 'detects ssh on standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
- stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url([config.user, '@', config.host, ':gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects ssh on non-standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
- stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, '/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on non-standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, ':3000/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'works with relative_url_root' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
end
context 'submodule on github.com' do
it 'detects ssh' do
stub_url('git@github.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://github.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
context 'submodule on gitlab.com' do
it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
context 'submodule on unsupported' do
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
new file mode 100644
index 00000000000..21e0e74e008
--- /dev/null
+++ b/spec/helpers/todos_helper_spec.rb
@@ -0,0 +1,57 @@
+require "spec_helper"
+
+describe TodosHelper do
+ include GitlabRoutingHelper
+
+ describe '#todo_target_path' do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+ let(:mr_todo) { build(:todo, project: project, target: merge_request) }
+ let(:issue_todo) { build(:todo, project: project, target: issue) }
+ let(:note_todo) { build(:todo, project: project, target: issue, note: note) }
+ let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) }
+
+ it 'returns correct path to the todo MR' do
+ expect(todo_target_path(mr_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}")
+ end
+
+ it 'returns correct path to the todo issue' do
+ expect(todo_target_path(issue_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}")
+ end
+
+ it 'returns correct path to the todo note' do
+ expect(todo_target_path(note_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}")
+ end
+
+ it 'returns correct path to build_todo MR when pipeline failed' do
+ expect(todo_target_path(build_failed_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines")
+ end
+ end
+
+ describe '#todo_projects_options' do
+ let(:projects) { create_list(:empty_project, 3) }
+ let(:user) { create(:user) }
+
+ it 'returns users authorised projects in json format' do
+ projects.first.add_developer(user)
+ projects.second.add_developer(user)
+
+ allow(helper).to receive(:current_user).and_return(user)
+
+ expected_results = [
+ { 'id' => '', 'text' => 'Any Project' },
+ { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace },
+ { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace }
+ ]
+
+ expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
+ end
+ end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
new file mode 100644
index 00000000000..889fe441171
--- /dev/null
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe VersionCheckHelper do
+ describe '#version_status_badge' do
+ it 'should return nil if not dev environment and not enabled' do
+ allow(Rails.env).to receive(:production?) { false }
+ allow(current_application_settings).to receive(:version_check_enabled) { false }
+
+ expect(helper.version_status_badge).to be(nil)
+ end
+
+ context 'when production and enabled' do
+ before do
+ allow(Rails.env).to receive(:production?) { true }
+ allow(current_application_settings).to receive(:version_check_enabled) { true }
+ allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
+
+ @image_tag = helper.version_status_badge
+ end
+
+ it 'should return an image tag' do
+ expect(@image_tag).to match(/^<img/)
+ end
+
+ it 'should have a js prefixed css class' do
+ expect(@image_tag).to match(/class="js-version-status-badge"/)
+ end
+
+ it 'should have a VersionCheck url as the src' do
+ expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/)
+ end
+ end
+ end
+end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index baab30f482f..374517fec37 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -12,43 +12,77 @@ describe '6_validations', lib: true do
FileUtils.rm_rf('tmp/tests/paths')
end
- context 'with correct settings' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d')
+ describe 'validate_storages_config' do
+ context 'with correct settings' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ end
+
+ it 'passes through' do
+ expect { validate_storages_config }.not_to raise_error
+ end
end
- it 'passes through' do
- expect { validate_storages }.not_to raise_error
+ context 'with invalid storage names' do
+ before do
+ mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
+ end
+
+ it 'throws an error' do
+ expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+ end
end
- end
- context 'with invalid storage names' do
- before do
- mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c')
+ context 'with incomplete settings' do
+ before do
+ mock_storages('foo' => {})
+ end
+
+ it 'throws an error suggesting the user to update its settings' do
+ expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
+ end
end
- it 'throws an error' do
- expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+ context 'with deprecated settings structure' do
+ before do
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c')
+ end
+
+ it 'throws an error suggesting the user to update its settings' do
+ expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
+ end
end
end
- context 'with nested storage paths' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d')
- end
+ describe 'validate_storages_paths' do
+ context 'with correct settings' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ end
- it 'throws an error' do
- expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+ it 'passes through' do
+ expect { validate_storages_paths }.not_to raise_error
+ end
end
- end
- context 'with similar but un-nested storage paths' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2')
+ context 'with nested storage paths' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
+ end
+
+ it 'throws an error' do
+ expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+ end
end
- it 'passes through' do
- expect { validate_storages }.not_to raise_error
+ context 'with similar but un-nested storage paths' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
+ end
+
+ it 'passes through' do
+ expect { validate_storages_paths }.not_to raise_error
+ end
end
end
diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
new file mode 100644
index 00000000000..570754621f3
--- /dev/null
+++ b/spec/initializers/8_metrics_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require_relative '../../config/initializers/8_metrics'
+
+describe 'instrument_classes', lib: true do
+ let(:config) { double(:config) }
+
+ before do
+ allow(config).to receive(:instrument_method)
+ allow(config).to receive(:instrument_methods)
+ allow(config).to receive(:instrument_instance_methods)
+ end
+
+ it 'can autoload and instrument all files' do
+ expect { instrument_classes(config) }.not_to raise_error
+ end
+end
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
new file mode 100644
index 00000000000..74bdbb01166
--- /dev/null
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+require_relative '../../config/initializers/doorkeeper'
+
+describe Doorkeeper.configuration do
+ describe '#default_scopes' do
+ it 'matches Gitlab::Auth::DEFAULT_SCOPES' do
+ expect(subject.default_scopes).to eq Gitlab::Auth::DEFAULT_SCOPES
+ end
+ end
+
+ describe '#optional_scopes' do
+ it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
+ expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
+ end
+ end
+
+ describe '#resource_owner_authenticator' do
+ subject { controller.instance_exec(&Doorkeeper.configuration.authenticate_resource_owner) }
+
+ let(:controller) { double }
+
+ before do
+ allow(controller).to receive(:current_user).and_return(current_user)
+ allow(controller).to receive(:session).and_return({})
+ allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path'))
+ allow(controller).to receive(:redirect_to)
+ allow(controller).to receive(:new_user_session_url).and_return('/login')
+ end
+
+ context 'with a user present' do
+ let(:current_user) { create(:user) }
+
+ it 'returns the user' do
+ expect(subject).to eq current_user
+ end
+
+ it 'does not redirect' do
+ expect(controller).not_to receive(:redirect_to)
+
+ subject
+ end
+
+ it 'does not store the return path' do
+ subject
+
+ expect(controller.session).not_to include :user_return_to
+ end
+ end
+
+ context 'without a user present' do
+ let(:current_user) { nil }
+
+ # NOTE: this is required for doorkeeper-openid_connect
+ it 'returns nil' do
+ expect(subject).to eq nil
+ end
+
+ it 'redirects to the login form' do
+ expect(controller).to receive(:redirect_to).with('/login')
+
+ subject
+ end
+
+ it 'stores the return path' do
+ subject
+
+ expect(controller.session[:user_return_to]).to eq '/return-path'
+ end
+ end
+ end
+end
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb
deleted file mode 100644
index bb595162370..00000000000
--- a/spec/initializers/metrics_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-require_relative '../../config/initializers/metrics'
-
-describe 'instrument_classes', lib: true do
- let(:config) { double(:config) }
-
- before do
- allow(config).to receive(:instrument_method)
- allow(config).to receive(:instrument_methods)
- allow(config).to receive(:instrument_instance_methods)
- end
-
- it 'can autoload and instrument all files' do
- expect { instrument_classes(config) }.not_to raise_error
- end
-end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index ad7f032d1e5..65c97da2efd 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do
let(:secrets) { ActiveSupport::OrderedOptions.new }
+ HEX_KEY = /\h{128}/
+ RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
+
before do
allow(File).to receive(:write)
allow(File).to receive(:delete)
@@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:exit)
end
- context 'setting secret_key_base and otp_key_base' do
+ context 'setting secret keys' do
context 'when none of the secrets exist' do
before do
stub_env('SECRET_KEY_BASE', nil)
@@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:warn_missing_secret)
end
- it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+ it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
create_tokens
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
expect(keys.uniq).to eq(keys)
- expect(keys.map(&:length)).to all(eq(128))
+ expect(keys).to all(match(HEX_KEY))
+ end
+
+ it 'generates an RSA key for jws_private_key' do
+ create_tokens
+
+ keys = secrets.values_at(:jws_private_key)
+
+ expect(keys.uniq).to eq(keys)
+ expect(keys).to all(match(RSA_KEY))
end
it 'warns about the secrets to add to secrets.yml' do
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_key_base')
+ expect(self).to receive(:warn_missing_secret).with('jws_private_key')
create_tokens
end
@@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+ expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
end
create_tokens
@@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do
context 'when the other secrets all exist' do
before do
secrets.db_key_base = 'db_key_base'
+ secrets.jws_private_key = 'jws_private_key'
allow(File).to receive(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key')
@@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do
stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
+ secrets.jws_private_key = 'jws_private_key'
end
it 'does not issue a warning' do
@@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do
before do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
+ secrets.jws_private_key = 'jws_private_key'
end
it 'does not write any files' do
@@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do
expect(secrets.secret_key_base).to eq('secret_key_base')
expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
+ expect(secrets.jws_private_key).to eq('jws_private_key')
end
it 'deletes the .secret file' do
@@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do
expect(new_secrets['secret_key_base']).to eq('file_key')
expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
+ expect(new_secrets['jws_private_key']).to eq('jws_private_key')
end
create_tokens
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 290e47763eb..ff8b8daa347 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -27,7 +27,7 @@ describe 'trusted_proxies', lib: true do
context 'with private IP ranges added' do
before do
- set_trusted_proxies([ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ])
+ set_trusted_proxies(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"])
end
it 'filters out private and local IPs' do
@@ -39,7 +39,7 @@ describe 'trusted_proxies', lib: true do
context 'with proxy IP added' do
before do
- set_trusted_proxies([ "60.98.25.47" ])
+ set_trusted_proxies(["60.98.25.47"])
end
it 'filters out proxy IP' do
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index fbd9bb9f0ff..3d922021978 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,7 +18,8 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
- "spyOnEvent": false
+ "spyOnEvent": false,
+ "ClassSpecHelper": false
},
"plugins": ["jasmine"],
"rules": {
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js
index 76b370b345b..76b370b345b 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js
index e6a6fc36ca1..e6a6fc36ca1 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
new file mode 100644
index 00000000000..a68bccb16f4
--- /dev/null
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -0,0 +1,58 @@
+require('~/extensions/array');
+require('jquery');
+require('jquery-ujs');
+require('~/ajax_loading_spinner');
+
+describe('Ajax Loading Spinner', () => {
+ const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ gl.AjaxLoadingSpinner.init();
+ });
+
+ it('change current icon with spinner icon and disable link while waiting ajax response', (done) => {
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+ const icon = ajaxLoadingSpinner.querySelector('i');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+
+ expect(icon).not.toHaveClass('fa-trash-o');
+ expect(icon).toHaveClass('fa-spinner');
+ expect(icon).toHaveClass('fa-spin');
+ expect(icon.dataset.icon).toEqual('fa-trash-o');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
+
+ req.complete({});
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+
+ it('use original icon again and enabled the link after complete the ajax request', (done) => {
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+ req.complete({});
+
+ const icon = ajaxLoadingSpinner.querySelector('i');
+ expect(icon).toHaveClass('fa-trash-o');
+ expect(icon).not.toHaveClass('fa-spinner');
+ expect(icon).not.toHaveClass('fa-spin');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index e5826f9c29f..ea7753c7a1d 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,11 +1,10 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
-/* global AwardsHandler */
-require('~/awards_handler');
-require('./fixtures/emoji_menu');
+import Cookies from 'js-cookie';
+import AwardsHandler from '~/awards_handler';
(function() {
- var awardsHandler, lazyAssert, urlRoot;
+ var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
awardsHandler = null;
@@ -13,14 +12,6 @@ require('./fixtures/emoji_menu');
window.gon || (window.gon = {});
- gl.emojiAliases = function() {
- return {
- '+1': 'thumbsup',
- '-1': 'thumbsdown'
- };
- };
-
- gon.award_menu_url = '/emojis';
urlRoot = gon.relative_url_root;
lazyAssert = function(done, assertFn) {
@@ -32,22 +23,37 @@ require('./fixtures/emoji_menu');
};
describe('AwardsHandler', function() {
- preloadFixtures('issues/open-issue.html.raw');
+ preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function() {
- loadFixtures('issues/open-issue.html.raw');
+ loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
return cb();
};
})(this));
- spyOn(jQuery, 'get').and.callFake(function(req, cb) {
- return cb(window.emojiMenu);
- });
+
+ let isEmojiMenuBuilt = false;
+ openAndWaitForEmojiMenu = function() {
+ return new Promise((resolve, reject) => {
+ if (isEmojiMenuBuilt) {
+ resolve();
+ } else {
+ $('.js-add-award').eq(0).click();
+ const $menu = $('.emoji-menu');
+ $menu.one('build-emoji-menu-finish', () => {
+ isEmojiMenuBuilt = true;
+ resolve();
+ });
+ }
+ });
+ };
});
afterEach(function() {
// restore original url root value
gon.relative_url_root = urlRoot;
+
+ awardsHandler.destroy();
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
@@ -62,10 +68,9 @@ require('./fixtures/emoji_menu');
});
});
it('should also show emoji menu for the smiley icon in notes', function(done) {
- $('.note-action-button').click();
+ $('.js-add-award.note-action-button').click();
return lazyAssert(done, function() {
- var $emojiMenu;
- $emojiMenu = $('.emoji-menu');
+ var $emojiMenu = $('.emoji-menu');
return expect($emojiMenu.length).toBe(1);
});
});
@@ -86,7 +91,7 @@ require('./fixtures/emoji_menu');
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
expect($emojiButton.length).toBe(1);
expect($emojiButton.next('.js-counter').text()).toBe('1');
return expect($votesBlock.hasClass('hidden')).toBe(false);
@@ -96,14 +101,14 @@ require('./fixtures/emoji_menu');
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
return expect($emojiButton.length).toBe(0);
});
return it('should decrement the emoji counter', function() {
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
$emojiButton.next('.js-counter').text(5);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
expect($emojiButton.length).toBe(1);
@@ -120,8 +125,8 @@ require('./fixtures/emoji_menu');
var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
- $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
+ $thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').parent();
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
expect($thumbsUpEmoji.hasClass('active')).toBe(true);
expect($thumbsDownEmoji.hasClass('active')).toBe(false);
@@ -138,9 +143,9 @@ require('./fixtures/emoji_menu');
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
- expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
- awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
- return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
+ expect($votesBlock.find('[data-name=fire]').length).toBe(1);
+ awardsHandler.removeEmoji($votesBlock.find('[data-name=fire]').closest('button'));
+ return expect($votesBlock.find('[data-name=fire]').length).toBe(0);
});
});
describe('::addYouToUserList', function() {
@@ -148,7 +153,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
@@ -158,7 +163,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
@@ -170,7 +175,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -181,7 +186,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -190,42 +195,111 @@ require('./fixtures/emoji_menu');
});
});
describe('search', function() {
- return it('should filter the emoji', function() {
- $('.js-add-award').eq(0).click();
- expect($('[data-emoji=angel]').is(':visible')).toBe(true);
- expect($('[data-emoji=anger]').is(':visible')).toBe(true);
- $('#emoji_search').val('ali').trigger('keyup');
- expect($('[data-emoji=angel]').is(':visible')).toBe(false);
- expect($('[data-emoji=anger]').is(':visible')).toBe(false);
- return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
+ return it('should filter the emoji', function(done) {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ expect($('[data-name=angel]').is(':visible')).toBe(true);
+ expect($('[data-name=anger]').is(':visible')).toBe(true);
+ $('#emoji_search').val('ali').trigger('input');
+ expect($('[data-name=angel]').is(':visible')).toBe(false);
+ expect($('[data-name=anger]').is(':visible')).toBe(false);
+ expect($('[data-name=alien]').is(':visible')).toBe(true);
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
});
});
- return describe('emoji menu', function() {
- var openEmojiMenuAndAddEmoji, selector;
- selector = '[data-emoji=sunglasses]';
- openEmojiMenuAndAddEmoji = function() {
- var $block, $emoji, $menu;
- $('.js-add-award').eq(0).click();
- $menu = $('.emoji-menu');
- $block = $('.js-awards-block');
- $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
- expect($emoji.length).toBe(1);
- expect($block.find(selector).length).toBe(0);
- $emoji.click();
- expect($menu.hasClass('.is-visible')).toBe(false);
- return expect($block.find(selector).length).toBe(1);
+ describe('emoji menu', function() {
+ const emojiSelector = '[data-name="sunglasses"]';
+ const openEmojiMenuAndAddEmoji = function() {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ const $menu = $('.emoji-menu');
+ const $block = $('.js-awards-block');
+ const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
+
+ expect($emoji.length).toBe(1);
+ expect($block.find(emojiSelector).length).toBe(0);
+ $emoji.click();
+ expect($menu.hasClass('.is-visible')).toBe(false);
+ expect($block.find(emojiSelector).length).toBe(1);
+ });
};
- it('should add selected emoji to awards block', function() {
- return openEmojiMenuAndAddEmoji();
+ it('should add selected emoji to awards block', function(done) {
+ return openEmojiMenuAndAddEmoji()
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
});
- return it('should remove already selected emoji', function() {
- var $block, $emoji;
- openEmojiMenuAndAddEmoji();
- $('.js-add-award').eq(0).click();
- $block = $('.js-awards-block');
- $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
- $emoji.click();
- return expect($block.find(selector).length).toBe(0);
+ it('should remove already selected emoji', function(done) {
+ return openEmojiMenuAndAddEmoji()
+ .then(() => {
+ $('.js-add-award').eq(0).click();
+ const $block = $('.js-awards-block');
+ const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
+ $emoji.click();
+ expect($block.find(emojiSelector).length).toBe(0);
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
+ });
+ });
+
+ describe('frequently used emojis', function() {
+ beforeEach(() => {
+ // Clear it out
+ Cookies.set('frequently_used_emojis', '');
+ });
+
+ it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', function(done) {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ const emojiMenu = document.querySelector('.emoji-menu');
+ Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => {
+ expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used');
+ });
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
+ });
+
+ it('should have any frequently used section when there are frequently used emojis', function(done) {
+ awardsHandler.addEmojiToFrequentlyUsedList('8ball');
+
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ const emojiMenu = document.querySelector('.emoji-menu');
+ const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title =>
+ title.textContent.trim().toLowerCase() === 'frequently used'
+ );
+
+ expect(hasFrequentlyUsedHeading).toBe(true);
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Failed to open and build emoji menu: ${err.message}`);
+ });
+ });
+
+ it('should disregard invalid frequently used emoji that are being attempted to be added', function() {
+ awardsHandler.addEmojiToFrequentlyUsedList('8ball');
+ awardsHandler.addEmojiToFrequentlyUsedList('invalid_emoji');
+ awardsHandler.addEmojiToFrequentlyUsedList('grinning');
+
+ expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
+ });
+
+ it('should disregard invalid frequently used emoji already set in cookie', function() {
+ Cookies.set('frequently_used_emojis', '8ball,invalid_emoji,grinning');
+
+ expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
});
});
});
diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js
new file mode 100644
index 00000000000..dd9ab33289f
--- /dev/null
+++ b/spec/javascripts/behaviors/bind_in_out_spec.js
@@ -0,0 +1,189 @@
+import BindInOut from '~/behaviors/bind_in_out';
+import ClassSpecHelper from '../helpers/class_spec_helper';
+
+describe('BindInOut', function () {
+ describe('.constructor', function () {
+ beforeEach(function () {
+ this.in = {};
+ this.out = {};
+
+ this.bindInOut = new BindInOut(this.in, this.out);
+ });
+
+ it('should set .in', function () {
+ expect(this.bindInOut.in).toBe(this.in);
+ });
+
+ it('should set .out', function () {
+ expect(this.bindInOut.out).toBe(this.out);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.bindInOut.eventWrapper).toEqual({});
+ });
+
+ describe('if .in is an input', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'INPUT' });
+ });
+
+ it('should set .eventType to keyup ', function () {
+ expect(this.bindInOut.eventType).toEqual('keyup');
+ });
+ });
+
+ describe('if .in is a textarea', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' });
+ });
+
+ it('should set .eventType to keyup ', function () {
+ expect(this.bindInOut.eventType).toEqual('keyup');
+ });
+ });
+
+ describe('if .in is not an input or textarea', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'SELECT' });
+ });
+
+ it('should set .eventType to change ', function () {
+ expect(this.bindInOut.eventType).toEqual('change');
+ });
+ });
+ });
+
+ describe('.addEvents', function () {
+ beforeEach(function () {
+ this.in = jasmine.createSpyObj('in', ['addEventListener']);
+
+ this.bindInOut = new BindInOut(this.in);
+
+ this.addEvents = this.bindInOut.addEvents();
+ });
+
+ it('should set .eventWrapper.updateOut', function () {
+ expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.in.addEventListener)
+ .toHaveBeenCalledWith(
+ this.bindInOut.eventType,
+ this.bindInOut.eventWrapper.updateOut,
+ );
+ });
+
+ it('should return the instance', function () {
+ expect(this.addEvents).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.updateOut', function () {
+ beforeEach(function () {
+ this.in = { value: 'the-value' };
+ this.out = { textContent: 'not-the-value' };
+
+ this.bindInOut = new BindInOut(this.in, this.out);
+
+ this.updateOut = this.bindInOut.updateOut();
+ });
+
+ it('should set .out.textContent to .in.value', function () {
+ expect(this.out.textContent).toBe(this.in.value);
+ });
+
+ it('should return the instance', function () {
+ expect(this.updateOut).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.removeEvents', function () {
+ beforeEach(function () {
+ this.in = jasmine.createSpyObj('in', ['removeEventListener']);
+ this.updateOut = () => {};
+
+ this.bindInOut = new BindInOut(this.in);
+ this.bindInOut.eventWrapper.updateOut = this.updateOut;
+
+ this.removeEvents = this.bindInOut.removeEvents();
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.in.removeEventListener)
+ .toHaveBeenCalledWith(
+ this.bindInOut.eventType,
+ this.updateOut,
+ );
+ });
+
+ it('should return the instance', function () {
+ expect(this.removeEvents).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.initAll', function () {
+ beforeEach(function () {
+ this.ins = [0, 1, 2];
+ this.instances = [];
+
+ spyOn(document, 'querySelectorAll').and.returnValue(this.ins);
+ spyOn(Array.prototype, 'map').and.callThrough();
+ spyOn(BindInOut, 'init');
+
+ this.initAll = BindInOut.initAll();
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll');
+
+ it('should call .querySelectorAll', function () {
+ expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]');
+ });
+
+ it('should call .map', function () {
+ expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it('should call .init for each element', function () {
+ expect(BindInOut.init.calls.count()).toEqual(3);
+ });
+
+ it('should return an array of instances', function () {
+ expect(this.initAll).toEqual(jasmine.any(Array));
+ });
+ });
+
+ describe('.init', function () {
+ beforeEach(function () {
+ spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; });
+ spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; });
+
+ this.init = BindInOut.init({}, {});
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init');
+
+ it('should call .addEvents', function () {
+ expect(BindInOut.prototype.addEvents).toHaveBeenCalled();
+ });
+
+ it('should call .updateOut', function () {
+ expect(BindInOut.prototype.updateOut).toHaveBeenCalled();
+ });
+
+ describe('if no anOut is provided', function () {
+ beforeEach(function () {
+ this.anIn = { dataset: { bindIn: 'the-data-bind-in' } };
+
+ spyOn(document, 'querySelector');
+
+ BindInOut.init(this.anIn);
+ });
+
+ it('should call .querySelector', function () {
+ expect(document.querySelector)
+ .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
new file mode 100644
index 00000000000..c1179e572ae
--- /dev/null
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -0,0 +1,107 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('CreateBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ // selectors
+ const createBranchSel = '.js-new-branch-btn';
+ const backBtnSel = '.dropdown-menu-back';
+ const cancelBtnSel = '.js-cancel-branch-btn';
+ const branchNameSel = '#new_branch_name';
+ const branchName = 'new_name';
+ let dropdown;
+
+ function createDropdown() {
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ const projectBranches = getJSONFixture('project_branches.json');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ return dropdown;
+ }
+
+ function createBranchBtn() {
+ return document.querySelector(createBranchSel);
+ }
+
+ function backBtn() {
+ return document.querySelector(backBtnSel);
+ }
+
+ function cancelBtn() {
+ return document.querySelector(cancelBtnSel);
+ }
+
+ function branchNameEl() {
+ return document.querySelector(branchNameSel);
+ }
+
+ function changeBranchName(text) {
+ branchNameEl().value = text;
+ branchNameEl().dispatchEvent(new Event('change'));
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch name is empty', () => {
+ expect(createBranchBtn()).toBeDisabled();
+ });
+
+ it('enable submit when branch name is present', () => {
+ changeBranchName(branchName);
+
+ expect(createBranchBtn()).not.toBeDisabled();
+ });
+
+ it('resets the form when cancel btn is clicked and triggers dropdownback', () => {
+ const spyBackEvent = spyOnEvent(backBtnSel, 'click');
+ changeBranchName(branchName);
+
+ cancelBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ expect(spyBackEvent).toHaveBeenTriggered();
+ });
+
+ it('resets the form when back btn is clicked', () => {
+ changeBranchName(branchName);
+
+ backBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ describe('new branch creation', () => {
+ beforeEach(() => {
+ changeBranchName(branchName);
+ });
+ it('sets the new branch name and updates the dropdown', () => {
+ spyOn(dropdown, 'setNewBranch');
+
+ createBranchBtn().click();
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+
+ it('resets the form', () => {
+ createBranchBtn().click();
+
+ expect(branchNameEl()).toHaveValue('');
+ });
+
+ it('is triggered with enter keypress', () => {
+ spyOn(dropdown, 'setNewBranch');
+ const enterEvent = new Event('keydown');
+ enterEvent.which = 13;
+ branchNameEl().dispatchEvent(enterEvent);
+
+ expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
new file mode 100644
index 00000000000..4fb79663c51
--- /dev/null
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -0,0 +1,119 @@
+require('~/gl_dropdown');
+require('~/lib/utils/type_utility');
+require('~/blob/create_branch_dropdown');
+require('~/blob/target_branch_dropdown');
+
+describe('TargetBranchDropdown', () => {
+ const fixtureTemplate = 'static/target_branch_dropdown.html.raw';
+ let dropdown;
+
+ function createDropdown() {
+ const projectBranches = getJSONFixture('project_branches.json');
+ const dropdownEl = document.querySelector('.js-project-branches-dropdown');
+ dropdown = new gl.TargetBranchDropDown(dropdownEl);
+ dropdown.cachedRefs = projectBranches;
+ dropdown.refreshData();
+ return dropdown;
+ }
+
+ function submitBtn() {
+ return document.querySelector('button[type="submit"]');
+ }
+
+ function searchField() {
+ return document.querySelector('.dropdown-page-one .dropdown-input-field');
+ }
+
+ function element() {
+ return document.querySelectorAll('div.dropdown-content li a');
+ }
+
+ function elementAtIndex(index) {
+ return element()[index];
+ }
+
+ function clickElementAtIndex(index) {
+ elementAtIndex(index).click();
+ }
+
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ createDropdown();
+ });
+
+ it('disable submit when branch is not selected', () => {
+ document.querySelector('input[name="target_branch"]').value = null;
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toEqual('');
+ });
+
+ it('enable submit when a branch is selected', () => {
+ clickElementAtIndex(1);
+
+ expect(submitBtn().getAttribute('disabled')).toBe(null);
+ });
+
+ it('triggers change.branch event on a branch click', () => {
+ spyOnEvent(dropdown.$dropdown, 'change.branch');
+ clickElementAtIndex(0);
+
+ expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown);
+ });
+
+ describe('#dropdownData', () => {
+ it('cache the refs', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.cachedRefs = null;
+
+ dropdown.dropdownData(refs);
+
+ expect(dropdown.cachedRefs).toEqual(refs);
+ });
+
+ it('returns the Branches with the newBranch and defaultBranch', () => {
+ const refs = dropdown.cachedRefs;
+ dropdown.branchInput.value = 'master';
+ dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' };
+
+ const branches = dropdown.dropdownData(refs).Branches;
+
+ expect(branches.length).toEqual(4);
+ expect(branches[0]).toEqual(dropdown.newBranch);
+ expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' });
+ expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' });
+ expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' });
+ });
+ });
+
+ describe('#setNewBranch', () => {
+ it('adds the new branch and select it', () => {
+ const branchName = 'new_branch';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(elementAtIndex(0)).toHaveClass('is-active');
+ expect(elementAtIndex(0)).toContainHtml(branchName);
+ });
+
+ it("doesn't add a new branch if already exists in the list", () => {
+ const branchName = elementAtIndex(0).text;
+ const initialLength = element().length;
+
+ dropdown.setNewBranch(branchName);
+
+ expect(element().length).toEqual(initialLength);
+ });
+
+ it('clears the search filter', () => {
+ const branchName = elementAtIndex(0).text;
+ searchField().value = 'searching';
+
+ dropdown.setNewBranch(branchName);
+
+ expect(searchField().value).toEqual('');
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
new file mode 100644
index 00000000000..47baf83512f
--- /dev/null
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -0,0 +1,93 @@
+/* global BoardService */
+import Vue from 'vue';
+import '~/boards/stores/boards_store';
+import boardBlankState from '~/boards/components/board_blank_state';
+import './mock_data';
+
+describe('Boards blank state', () => {
+ let vm;
+ let fail = false;
+
+ beforeEach((done) => {
+ const Comp = Vue.extend(boardBlankState);
+
+ gl.issueBoards.BoardsStore.create();
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+
+ spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
+ if (fail) {
+ reject();
+ } else {
+ resolve({
+ json() {
+ return [{
+ id: 1,
+ title: 'To Do',
+ label: { id: 1 },
+ }, {
+ id: 2,
+ title: 'Doing',
+ label: { id: 2 },
+ }];
+ },
+ });
+ }
+ }));
+
+ vm = new Comp();
+
+ setTimeout(() => {
+ vm.$mount();
+ done();
+ });
+ });
+
+ it('renders pre-defined labels', () => {
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li').length,
+ ).toBe(2);
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim(),
+ ).toEqual('To Do');
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim(),
+ ).toEqual('Doing');
+ });
+
+ it('clears blank state', (done) => {
+ vm.$el.querySelector('.btn-default').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('creates pre-defined labels', (done) => {
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do');
+ expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing');
+
+ done();
+ });
+ });
+
+ it('resets the store if request fails', (done) => {
+ fail = true;
+
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy();
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
new file mode 100644
index 00000000000..be31f644e20
--- /dev/null
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -0,0 +1,168 @@
+/* global Vue */
+/* global List */
+/* global ListLabel */
+/* global listObj */
+/* global boardsMockInterceptor */
+/* global BoardService */
+
+require('~/boards/models/list');
+require('~/boards/models/label');
+require('~/boards/stores/boards_store');
+const boardCard = require('~/boards/components/board_card').default;
+require('./mock_data');
+
+describe('Issue card', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
+
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+ gl.issueBoards.BoardsStore.detail.issue = {};
+
+ const BoardCardComp = Vue.extend(boardCard);
+ const list = new List(listObj);
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+
+ setTimeout(() => {
+ list.issues[0].labels.push(label1);
+
+ vm = new BoardCardComp({
+ propsData: {
+ list,
+ issue: list.issues[0],
+ issueLinkBase: '/',
+ disabled: false,
+ index: 0,
+ rootPath: '/',
+ },
+ }).$mount();
+ done();
+ }, 0);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('returns false when detailIssue is empty', () => {
+ expect(vm.issueDetailVisible).toBe(false);
+ });
+
+ it('returns true when detailIssue is equal to card issue', () => {
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
+
+ expect(vm.issueDetailVisible).toBe(true);
+ });
+
+ it('adds user-can-drag class if not disabled', () => {
+ expect(vm.$el.classList.contains('user-can-drag')).toBe(true);
+ });
+
+ it('does not add user-can-drag class disabled', (done) => {
+ vm.disabled = true;
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('user-can-drag')).toBe(false);
+ done();
+ }, 0);
+ });
+
+ it('does not add disabled class', () => {
+ expect(vm.$el.classList.contains('is-disabled')).toBe(false);
+ });
+
+ it('adds disabled class is disabled is true', (done) => {
+ vm.disabled = true;
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('is-disabled')).toBe(true);
+ done();
+ }, 0);
+ });
+
+ describe('mouse events', () => {
+ const triggerEvent = (eventName, el = vm.$el) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(eventName, true, true, window, 1, 0, 0, 0, 0, false, false,
+ false, false, 0, null);
+
+ el.dispatchEvent(event);
+ };
+
+ it('sets showDetail to true on mousedown', () => {
+ triggerEvent('mousedown');
+
+ expect(vm.showDetail).toBe(true);
+ });
+
+ it('sets showDetail to false on mousemove', () => {
+ triggerEvent('mousedown');
+
+ expect(vm.showDetail).toBe(true);
+
+ triggerEvent('mousemove');
+
+ expect(vm.showDetail).toBe(false);
+ });
+
+ it('does not set detail issue if showDetail is false', () => {
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if link is clicked', () => {
+ triggerEvent('mouseup', vm.$el.querySelector('a'));
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if button is clicked', () => {
+ triggerEvent('mouseup', vm.$el.querySelector('button'));
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if showDetail is false after mouseup', () => {
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('sets detail issue to card issue on mouse up', () => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
+ });
+
+ it('adds active class if detail issue is set', (done) => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ done();
+ }, 0);
+ });
+
+ it('resets detail issue to empty if already set', () => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
new file mode 100644
index 00000000000..4999933c0c1
--- /dev/null
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -0,0 +1,190 @@
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global listObj */
+
+import Vue from 'vue';
+import boardNewIssue from '~/boards/components/board_new_issue';
+
+require('~/boards/models/list');
+require('./mock_data');
+
+describe('Issue boards new issue form', () => {
+ let vm;
+ let list;
+ const promiseReturn = {
+ json() {
+ return {
+ iid: 100,
+ };
+ },
+ };
+ const submitIssue = () => {
+ vm.$el.querySelector('.btn-success').click();
+ };
+
+ beforeEach((done) => {
+ const BoardNewIssueComp = Vue.extend(boardNewIssue);
+
+ Vue.http.interceptors.push(boardsMockInterceptor);
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+ gl.IssueBoardsApp = new Vue();
+
+ setTimeout(() => {
+ list = new List(listObj);
+
+ spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => {
+ if (vm.title === 'error') {
+ reject();
+ } else {
+ resolve(promiseReturn);
+ }
+ }));
+
+ vm = new BoardNewIssueComp({
+ propsData: {
+ list,
+ },
+ }).$mount();
+
+ done();
+ }, 0);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('disables submit button if title is empty', () => {
+ expect(vm.$el.querySelector('.btn-success').disabled).toBe(true);
+ });
+
+ it('enables submit button if title is not empty', (done) => {
+ vm.title = 'Testing Title';
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
+ expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
+
+ done();
+ }, 0);
+ });
+
+ it('clears title after clicking cancel', (done) => {
+ vm.$el.querySelector('.btn-default').click();
+
+ setTimeout(() => {
+ expect(vm.title).toBe('');
+ done();
+ }, 0);
+ });
+
+ it('does not create new issue if title is empty', (done) => {
+ submitIssue();
+
+ setTimeout(() => {
+ expect(gl.boardService.newIssue).not.toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+
+ describe('submit success', () => {
+ it('creates new issue', (done) => {
+ vm.title = 'submit title';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(gl.boardService.newIssue).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+
+ it('enables button after submit', (done) => {
+ vm.title = 'submit issue';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true);
+ done();
+ }, 0);
+ });
+
+ it('clears title after submit', (done) => {
+ vm.title = 'submit issue';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(vm.title).toBe('');
+ done();
+ }, 0);
+ });
+
+ it('adds new issue to list after submit', (done) => {
+ vm.title = 'submit issue';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(list.issues.length).toBe(2);
+ expect(list.issues[1].title).toBe('submit issue');
+ expect(list.issues[1].subscribed).toBe(true);
+ done();
+ }, 0);
+ });
+
+ it('sets detail issue after submit', (done) => {
+ vm.title = 'submit issue';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue');
+ done();
+ });
+ });
+
+ it('sets detail list after submit', (done) => {
+ vm.title = 'submit issue';
+
+ setTimeout(() => {
+ submitIssue();
+
+ expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('submit error', () => {
+ it('removes issue', (done) => {
+ vm.title = 'error';
+
+ setTimeout(() => {
+ submitIssue();
+
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ done();
+ }, 500);
+ }, 0);
+ });
+
+ it('shows error', (done) => {
+ vm.title = 'error';
+ submitIssue();
+
+ setTimeout(() => {
+ submitIssue();
+
+ setTimeout(() => {
+ expect(vm.error).toBe(true);
+ done();
+ }, 500);
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
new file mode 100644
index 00000000000..1d1069600fc
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -0,0 +1,232 @@
+/* eslint-disable comma-dangle, one-var, no-unused-vars */
+/* global Vue */
+/* global BoardService */
+/* global boardsMockInterceptor */
+/* global Cookies */
+/* global listObj */
+/* global listObjDuplicate */
+/* global ListIssue */
+
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
+
+describe('Store', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+
+ spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
+ resolve();
+ }));
+
+ Cookies.set('issue_board_welcome_hidden', 'false', {
+ expires: 365 * 10,
+ path: ''
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
+
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(list.id).toBe(1);
+ });
+
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+
+ expect(list).toBeDefined();
+ });
+
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('persists new list', (done) => {
+ gl.issueBoards.BoardsStore.new({
+ title: 'Test',
+ type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+ });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ expect(list.id).toBe(1);
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('check for blank state adding', () => {
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('check for blank state not adding', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+ });
+
+ it('check for blank state adding when done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
+ });
+
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
+
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
+
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.removeList(1, 'label');
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ it('moves the position of lists', () => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+
+ expect(listOne.position).toBe(1);
+ });
+
+ it('moves an issue from one list to another', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
+
+ done();
+ }, 0);
+ });
+
+ it('moves issue to top of another list', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ listOne.issues[0].id = 2;
+
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(2);
+ expect(listTwo.issues[0].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
+
+ done();
+ }, 0);
+ });
+
+ it('moves issue to bottom of another list', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ listOne.issues[0].id = 2;
+
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(2);
+ expect(listTwo.issues[1].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
+
+ done();
+ }, 0);
+ });
+
+ it('moves issue in list', (done) => {
+ const issue = new ListIssue({
+ title: 'Testing',
+ iid: 2,
+ confidential: false,
+ labels: []
+ });
+ const list = gl.issueBoards.BoardsStore.addList(listObj);
+
+ setTimeout(() => {
+ list.addIssue(issue);
+
+ expect(list.issues.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
+
+ expect(list.issues[0].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
deleted file mode 100644
index 9dd741a680b..00000000000
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ /dev/null
@@ -1,158 +0,0 @@
-/* eslint-disable comma-dangle, one-var, no-unused-vars */
-/* global Vue */
-/* global BoardService */
-/* global boardsMockInterceptor */
-/* global Cookies */
-/* global listObj */
-/* global listObjDuplicate */
-
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
-
-describe('Store', () => {
- beforeEach(() => {
- Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
- gl.issueBoards.BoardsStore.create();
-
- Cookies.set('issue_board_welcome_hidden', 'false', {
- expires: 365 * 10,
- path: ''
- });
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
- });
-
- it('starts with a blank state', () => {
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
-
- describe('lists', () => {
- it('creates new list without persisting to DB', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- });
-
- it('finds list by ID', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
-
- expect(list.id).toBe(1);
- });
-
- it('finds list by type', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('type', 'label');
-
- expect(list).toBeDefined();
- });
-
- it('gets issue when new list added', (done) => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
-
- setTimeout(() => {
- expect(list.issues.length).toBe(1);
- expect(list.issues[0].id).toBe(1);
- done();
- }, 0);
- });
-
- it('persists new list', (done) => {
- gl.issueBoards.BoardsStore.new({
- title: 'Test',
- type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;'
- }
- });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
-
- setTimeout(() => {
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
- expect(list).toBeDefined();
- expect(list.id).toBe(1);
- expect(list.position).toBe(0);
- done();
- }, 0);
- });
-
- it('check for blank state adding', () => {
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
- });
-
- it('check for blank state not adding', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
- });
-
- it('check for blank state adding when done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'done'
- });
-
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
- });
-
- it('adds the blank state', () => {
- gl.issueBoards.BoardsStore.addBlankState();
-
- const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
- expect(list).toBeDefined();
- });
-
- it('removes list from state', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
-
- gl.issueBoards.BoardsStore.removeList(1, 'label');
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
-
- it('moves the position of lists', () => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj);
- const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
-
- gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
-
- expect(listOne.position).toBe(1);
- });
-
- it('moves an issue from one list to another', (done) => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj);
- const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
-
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
-
- setTimeout(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
-
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js
index 4340a571017..4340a571017 100644
--- a/spec/javascripts/boards/issue_card_spec.js.es6
+++ b/spec/javascripts/boards/issue_card_spec.js
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
new file mode 100644
index 00000000000..c96dfe94a4a
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js
@@ -0,0 +1,98 @@
+/* eslint-disable comma-dangle */
+/* global BoardService */
+/* global ListIssue */
+
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
+
+describe('Issue model', () => {
+ let issue;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [{
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing'
+ }]
+ });
+ });
+
+ it('has label', () => {
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('add new label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+ });
+
+ it('does not add existing label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'test',
+ color: 'blue',
+ description: 'bugs!'
+ });
+
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('finds label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ expect(label).toBeDefined();
+ });
+
+ it('removes label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ issue.removeLabel(label);
+ expect(issue.labels.length).toBe(0);
+ });
+
+ it('removes multiple labels', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+
+ issue.removeLabels([issue.labels[0], issue.labels[1]]);
+ expect(issue.labels.length).toBe(0);
+ });
+
+ it('sets position to infinity if no position is stored', () => {
+ expect(issue.position).toBe(Infinity);
+ });
+
+ it('sets position', () => {
+ const relativePositionIssue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ relative_position: 1,
+ labels: []
+ });
+
+ expect(relativePositionIssue.position).toBe(1);
+ });
+});
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
deleted file mode 100644
index aab4d9c501e..00000000000
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ /dev/null
@@ -1,82 +0,0 @@
-/* eslint-disable comma-dangle */
-/* global BoardService */
-/* global ListIssue */
-
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
-
-describe('Issue model', () => {
- let issue;
-
- beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
- gl.issueBoards.BoardsStore.create();
-
- issue = new ListIssue({
- title: 'Testing',
- iid: 1,
- confidential: false,
- labels: [{
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing'
- }]
- });
- });
-
- it('has label', () => {
- expect(issue.labels.length).toBe(1);
- });
-
- it('add new label', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!'
- });
- expect(issue.labels.length).toBe(2);
- });
-
- it('does not add existing label', () => {
- issue.addLabel({
- id: 2,
- title: 'test',
- color: 'blue',
- description: 'bugs!'
- });
-
- expect(issue.labels.length).toBe(1);
- });
-
- it('finds label', () => {
- const label = issue.findLabel(issue.labels[0]);
- expect(label).toBeDefined();
- });
-
- it('removes label', () => {
- const label = issue.findLabel(issue.labels[0]);
- issue.removeLabel(label);
- expect(issue.labels.length).toBe(0);
- });
-
- it('removes multiple labels', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!'
- });
- expect(issue.labels.length).toBe(2);
-
- issue.removeLabels([issue.labels[0], issue.labels[1]]);
- expect(issue.labels.length).toBe(0);
- });
-});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
new file mode 100644
index 00000000000..d49d3af33d9
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js
@@ -0,0 +1,109 @@
+/* eslint-disable comma-dangle */
+/* global Vue */
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global ListIssue */
+/* global listObj */
+/* global listObjDuplicate */
+
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
+
+describe('List model', () => {
+ let list;
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+
+ list = new List(listObj);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('gets issues when created', (done) => {
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('saves list and returns ID', (done) => {
+ list = new List({
+ title: 'test',
+ label: {
+ id: 1,
+ title: 'test',
+ color: 'red'
+ }
+ });
+ list.save();
+
+ setTimeout(() => {
+ expect(list.id).toBe(1);
+ expect(list.type).toBe('label');
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('destroys the list', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ list.destroy();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('gets issue from list', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(issue).toBeDefined();
+ done();
+ }, 0);
+ });
+
+ it('removes issue', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(list.issues.length).toBe(1);
+ list.removeIssue(issue);
+ expect(list.issues.length).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('sends service request to update issue label', () => {
+ const listDup = new List(listObjDuplicate);
+ const issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [list.label, listDup.label]
+ });
+
+ list.issues.push(issue);
+ listDup.issues.push(issue);
+
+ spyOn(gl.boardService, 'moveIssue').and.callThrough();
+
+ listDup.updateIssueLabel(list, issue);
+
+ expect(gl.boardService.moveIssue)
+ .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
+ });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
deleted file mode 100644
index 4397a32fedc..00000000000
--- a/spec/javascripts/boards/list_spec.js.es6
+++ /dev/null
@@ -1,87 +0,0 @@
-/* eslint-disable comma-dangle */
-/* global Vue */
-/* global boardsMockInterceptor */
-/* global BoardService */
-/* global List */
-/* global listObj */
-
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
-
-describe('List model', () => {
- let list;
-
- beforeEach(() => {
- Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
- gl.issueBoards.BoardsStore.create();
-
- list = new List(listObj);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
- });
-
- it('gets issues when created', (done) => {
- setTimeout(() => {
- expect(list.issues.length).toBe(1);
- done();
- }, 0);
- });
-
- it('saves list and returns ID', (done) => {
- list = new List({
- title: 'test',
- label: {
- id: 1,
- title: 'test',
- color: 'red'
- }
- });
- list.save();
-
- setTimeout(() => {
- expect(list.id).toBe(1);
- expect(list.type).toBe('label');
- expect(list.position).toBe(0);
- done();
- }, 0);
- });
-
- it('destroys the list', (done) => {
- gl.issueBoards.BoardsStore.addList(listObj);
- list = gl.issueBoards.BoardsStore.findList('id', 1);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- list.destroy();
-
- setTimeout(() => {
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- done();
- }, 0);
- });
-
- it('gets issue from list', (done) => {
- setTimeout(() => {
- const issue = list.findIssue(1);
- expect(issue).toBeDefined();
- done();
- }, 0);
- });
-
- it('removes issue', (done) => {
- setTimeout(() => {
- const issue = list.findIssue(1);
- expect(list.issues.length).toBe(1);
- list.removeIssue(issue);
- expect(list.issues.length).toBe(0);
- done();
- }, 0);
- });
-});
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js
index 7a399b307ad..7a399b307ad 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js
diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js
index 1815847f3fa..1815847f3fa 100644
--- a/spec/javascripts/boards/modal_store_spec.js.es6
+++ b/spec/javascripts/boards/modal_store_spec.js
diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js
new file mode 100644
index 00000000000..48994b7c523
--- /dev/null
+++ b/spec/javascripts/bootstrap_jquery_spec.js
@@ -0,0 +1,42 @@
+/* eslint-disable space-before-function-paren, no-var */
+
+import '~/commons/bootstrap';
+
+(function() {
+ describe('Bootstrap jQuery extensions', function() {
+ describe('disable', function() {
+ beforeEach(function() {
+ return setFixtures('<input type="text" />');
+ });
+ it('adds the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveAttr('disabled', 'disabled');
+ });
+ return it('adds the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveClass('disabled');
+ });
+ });
+ return describe('enable', function() {
+ beforeEach(function() {
+ return setFixtures('<input type="text" disabled="disabled" class="disabled" />');
+ });
+ it('removes the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveAttr('disabled');
+ });
+ return it('removes the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveClass('disabled');
+ });
+ });
+ });
+}).call(window);
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js
index fa9f95e16cd..fa9f95e16cd 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
new file mode 100644
index 00000000000..549c7af8ea8
--- /dev/null
+++ b/spec/javascripts/build_spec.js
@@ -0,0 +1,177 @@
+/* eslint-disable no-new */
+/* global Build */
+
+require('~/lib/utils/datetime_utility');
+require('~/lib/utils/url_utility');
+require('~/build');
+require('~/breakpoints');
+require('vendor/jquery.nicescroll');
+
+describe('Build', () => {
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
+
+ preloadFixtures('builds/build-with-artifacts.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('builds/build-with-artifacts.html.raw');
+ spyOn($, 'ajax');
+ });
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ describe('setup', () => {
+ beforeEach(function () {
+ this.build = new Build();
+ });
+
+ it('copies build options', function () {
+ expect(this.build.pageUrl).toBe(BUILD_URL);
+ expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
+ expect(this.build.buildStatus).toBe('success');
+ expect(this.build.buildStage).toBe('test');
+ expect(this.build.state).toBe('');
+ });
+
+ it('only shows the jobs matching the current stage', () => {
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('selects the current stage in the build dropdown menu', () => {
+ expect($('.stage-selection').text()).toBe('test');
+ });
+
+ it('updates the jobs when the build dropdown changes', () => {
+ $('.stage-item:contains("build")').click();
+
+ expect($('.stage-selection').text()).toBe('build');
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('displays the remove date correctly', () => {
+ const removeDateElement = document.querySelector('.js-artifacts-remove');
+ expect(removeDateElement.innerText.trim()).toBe('1 year');
+ });
+ });
+
+ describe('initial build trace', () => {
+ beforeEach(() => {
+ new Build();
+ });
+
+ it('displays the initial build trace', () => {
+ expect($.ajax.calls.count()).toBe(1);
+ const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
+ expect(url).toBe(`${BUILD_URL}.json`);
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
+ });
+
+ it('removes the spinner', () => {
+ const [{ success, context }] = $.ajax.calls.argsFor(0);
+ success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
+
+ expect($('.js-build-refresh').length).toBe(0);
+ });
+ });
+
+ describe('running build', () => {
+ beforeEach(function () {
+ $('.js-build-options').data('buildStatus', 'running');
+ this.build = new Build();
+ spyOn(this.build, 'location').and.returnValue(BUILD_URL);
+ });
+
+ it('updates the build trace on an interval', function () {
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(2);
+ let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
+ expect(url).toBe(
+ `${BUILD_URL}/trace.json?state=`,
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>Update<span>',
+ status: 'running',
+ state: 'newstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.build.state).toBe('newstate');
+
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(3);
+ [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
+ expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`);
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.build.state).toBe('finalstate');
+ });
+
+ it('replaces the entire build trace', () => {
+ jasmine.clock().tick(4001);
+ let [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Update</span>',
+ status: 'running',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+ [{ success, context }] = $.ajax.calls.argsFor(2);
+ success.call(context, {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ });
+
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ });
+
+ it('reloads the page when the build is done', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ jasmine.clock().tick(4001);
+ const [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Final</span>',
+ status: 'passed',
+ append: true,
+ });
+
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
deleted file mode 100644
index 0bd50588f5a..00000000000
--- a/spec/javascripts/build_spec.js.es6
+++ /dev/null
@@ -1,183 +0,0 @@
-/* eslint-disable no-new */
-/* global Build */
-
-require('~/lib/utils/datetime_utility');
-require('~/lib/utils/url_utility');
-require('~/build');
-require('~/breakpoints');
-require('vendor/jquery.nicescroll');
-
-describe('Build', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
- // see spec/factories/ci/builds.rb
- const BUILD_TRACE = 'BUILD TRACE';
- // see lib/ci/ansi2html.rb
- const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({
- offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
- }));
-
- preloadFixtures('builds/build-with-artifacts.html.raw');
-
- beforeEach(() => {
- loadFixtures('builds/build-with-artifacts.html.raw');
- spyOn($, 'ajax');
- });
-
- describe('constructor', () => {
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- describe('setup', () => {
- beforeEach(function () {
- this.build = new Build();
- });
-
- it('copies build options', function () {
- expect(this.build.pageUrl).toBe(BUILD_URL);
- expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
- expect(this.build.buildStatus).toBe('success');
- expect(this.build.buildStage).toBe('test');
- expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE);
- });
-
- it('only shows the jobs matching the current stage', () => {
- expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
- expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
- expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
- });
-
- it('selects the current stage in the build dropdown menu', () => {
- expect($('.stage-selection').text()).toBe('test');
- });
-
- it('updates the jobs when the build dropdown changes', () => {
- $('.stage-item:contains("build")').click();
-
- expect($('.stage-selection').text()).toBe('build');
- expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
- expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
- expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
- });
-
- it('displays the remove date correctly', () => {
- const removeDateElement = document.querySelector('.js-artifacts-remove');
- expect(removeDateElement.innerText.trim()).toBe('1 year');
- });
- });
-
- describe('initial build trace', () => {
- beforeEach(() => {
- new Build();
- });
-
- it('displays the initial build trace', () => {
- expect($.ajax.calls.count()).toBe(1);
- const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
- expect(url).toBe(`${BUILD_URL}.json`);
- expect(dataType).toBe('json');
- expect(success).toEqual(jasmine.any(Function));
-
- success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
-
- expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
- });
-
- it('removes the spinner', () => {
- const [{ success, context }] = $.ajax.calls.argsFor(0);
- success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
-
- expect($('.js-build-refresh').length).toBe(0);
- });
- });
-
- describe('running build', () => {
- beforeEach(function () {
- $('.js-build-options').data('buildStatus', 'running');
- this.build = new Build();
- spyOn(this.build, 'location').and.returnValue(BUILD_URL);
- });
-
- it('updates the build trace on an interval', function () {
- jasmine.clock().tick(4001);
-
- expect($.ajax.calls.count()).toBe(2);
- let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
- expect(url).toBe(
- `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`,
- );
- expect(dataType).toBe('json');
- expect(success).toEqual(jasmine.any(Function));
-
- success.call(context, {
- html: '<span>Update<span>',
- status: 'running',
- state: 'newstate',
- append: true,
- });
-
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.build.state).toBe('newstate');
-
- jasmine.clock().tick(4001);
-
- expect($.ajax.calls.count()).toBe(3);
- [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
- expect(url).toBe(`${BUILD_URL}/trace.json?state=newstate`);
- expect(dataType).toBe('json');
- expect(success).toEqual(jasmine.any(Function));
-
- success.call(context, {
- html: '<span>More</span>',
- status: 'running',
- state: 'finalstate',
- append: true,
- });
-
- expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(this.build.state).toBe('finalstate');
- });
-
- it('replaces the entire build trace', () => {
- jasmine.clock().tick(4001);
- let [{ success, context }] = $.ajax.calls.argsFor(1);
- success.call(context, {
- html: '<span>Update</span>',
- status: 'running',
- append: true,
- });
-
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
- jasmine.clock().tick(4001);
- [{ success, context }] = $.ajax.calls.argsFor(2);
- success.call(context, {
- html: '<span>Different</span>',
- status: 'running',
- append: false,
- });
-
- expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
- expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
- });
-
- it('reloads the page when the build is done', () => {
- spyOn(gl.utils, 'visitUrl');
-
- jasmine.clock().tick(4001);
- const [{ success, context }] = $.ajax.calls.argsFor(1);
- success.call(context, {
- html: '<span>Final</span>',
- status: 'passed',
- append: true,
- });
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
- });
- });
- });
-});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
new file mode 100644
index 00000000000..82b00b4c1ec
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -0,0 +1,89 @@
+export default {
+ id: 73,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ path: '/root/review-app/pipelines/73',
+ details: {
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73',
+ },
+ duration: null,
+ finished_at: '2017-01-25T00:00:17.130Z',
+ stages: [{
+ name: 'build',
+ title: 'build: failed',
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73#build',
+ },
+ path: '/root/review-app/pipelines/73#build',
+ dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
+ }],
+ artifacts: [],
+ manual_actions: [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1463/play',
+ },
+ {
+ name: 'name',
+ path: '/root/review-app/builds/1490/play',
+ },
+ ],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ },
+ ref:
+ {
+ name: 'master',
+ path: '/root/review-app/tree/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ short_id: 'fbd79f04',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2017-01-16T12:13:57.000-05:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ },
+ retry_path: '/root/review-app/pipelines/73/retry',
+ created_at: '2017-01-16T17:13:59.800Z',
+ updated_at: '2017-01-25T00:00:17.132Z',
+};
diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6
deleted file mode 100644
index 188908d66bd..00000000000
--- a/spec/javascripts/commit/pipelines/mock_data.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-/* eslint-disable no-unused-vars */
-const pipeline = {
- id: 73,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- path: '/root/review-app/pipelines/73',
- details: {
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73',
- },
- duration: null,
- finished_at: '2017-01-25T00:00:17.130Z',
- stages: [{
- name: 'build',
- title: 'build: failed',
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73#build',
- },
- path: '/root/review-app/pipelines/73#build',
- dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
- }],
- artifacts: [],
- manual_actions: [
- {
- name: 'stop_review',
- path: '/root/review-app/builds/1463/play',
- },
- {
- name: 'name',
- path: '/root/review-app/builds/1490/play',
- },
- ],
- },
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- },
- ref:
- {
- name: 'master',
- path: '/root/review-app/tree/master',
- tag: false,
- branch: true,
- },
- commit: {
- id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
- short_id: 'fbd79f04',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2017-01-16T12:13:57.000-05:00',
- committer_name: 'Administrator',
- committer_email: 'admin@example.com',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- },
- retry_path: '/root/review-app/pipelines/73/retry',
- created_at: '2017-01-16T17:13:59.800Z',
- updated_at: '2017-01-25T00:00:17.132Z',
-};
-
-module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
new file mode 100644
index 00000000000..75efcc06585
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import PipelinesTable from '~/commit/pipelines/pipelines_table';
+import pipeline from './mock_data';
+
+describe('Pipelines table in Commits and Merge requests', () => {
+ preloadFixtures('static/pipelines_table.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/pipelines_table.html.raw');
+ });
+
+ describe('successfull request', () => {
+ describe('without pipelines', () => {
+ const pipelinesEmptyResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesEmptyResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesEmptyResponse,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ const component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 1);
+ });
+ });
+
+ describe('with pipelines', () => {
+ const pipelinesResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([pipeline]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesResponse,
+ );
+ });
+
+ it('should render a table with the received pipelines', (done) => {
+ const component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const pipelinesErrorResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesErrorResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesErrorResponse,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ const component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
deleted file mode 100644
index f09c57978a1..00000000000
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
+++ /dev/null
@@ -1,105 +0,0 @@
-/* global pipeline, Vue */
-
-require('~/flash');
-require('~/commit/pipelines/pipelines_store');
-require('~/commit/pipelines/pipelines_service');
-require('~/commit/pipelines/pipelines_table');
-require('~/vue_shared/vue_resource_interceptor');
-const pipeline = require('./mock_data');
-
-describe('Pipelines table in Commits and Merge requests', () => {
- preloadFixtures('static/pipelines_table.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/pipelines_table.html.raw');
- });
-
- describe('successfull request', () => {
- describe('without pipelines', () => {
- const pipelinesEmptyResponse = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(pipelinesEmptyResponse);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, pipelinesEmptyResponse,
- );
- });
-
- it('should render the empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
- setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
- done();
- }, 1);
- });
- });
-
- describe('with pipelines', () => {
- const pipelinesResponse = (request, next) => {
- next(request.respondWith(JSON.stringify([pipeline]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(pipelinesResponse);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, pipelinesResponse,
- );
- });
-
- it('should render a table with the received pipelines', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
- setTimeout(() => {
- expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
- done();
- }, 0);
- });
- });
- });
-
- describe('unsuccessfull request', () => {
- const pipelinesErrorResponse = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(pipelinesErrorResponse);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, pipelinesErrorResponse,
- );
- });
-
- it('should render empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
- setTimeout(() => {
- expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
deleted file mode 100644
index 94973419979..00000000000
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
+++ /dev/null
@@ -1,33 +0,0 @@
-const PipelinesStore = require('~/commit/pipelines/pipelines_store');
-
-describe('Store', () => {
- let store;
-
- beforeEach(() => {
- store = new PipelinesStore();
- });
-
- // unregister intervals and event handlers
- afterEach(() => gl.VueRealtimeListener.reset());
-
- it('should start with a blank state', () => {
- expect(store.state.pipelines.length).toBe(0);
- });
-
- it('should store an array of pipelines', () => {
- const pipelines = [
- {
- id: '1',
- name: 'pipeline',
- },
- {
- id: '2',
- name: 'pipeline_2',
- },
- ];
-
- store.storePipelines(pipelines);
-
- expect(store.state.pipelines.length).toBe(pipelines.length);
- });
-});
diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js
index 05260760c43..05260760c43 100644
--- a/spec/javascripts/commits_spec.js.es6
+++ b/spec/javascripts/commits_spec.js
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js
index d5eec10be42..d5eec10be42 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js
new file mode 100644
index 00000000000..84cf98c930a
--- /dev/null
+++ b/spec/javascripts/diff_comments_store_spec.js
@@ -0,0 +1,133 @@
+/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
+/* global CommentsStore */
+
+require('~/diff_notes/models/discussion');
+require('~/diff_notes/models/note');
+require('~/diff_notes/stores/comments');
+
+(() => {
+ function createDiscussion(noteId = 1, resolved = true) {
+ CommentsStore.create({
+ discussionId: 'a',
+ noteId,
+ canResolve: true,
+ resolved,
+ resolvedBy: 'test',
+ authorName: 'test',
+ authorAvatar: 'test',
+ noteTruncated: 'test...',
+ });
+ }
+
+ beforeEach(() => {
+ CommentsStore.state = {};
+ });
+
+ describe('New discussion', () => {
+ it('creates new discussion', () => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ });
+
+ it('creates new note in discussion', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ const discussion = CommentsStore.state['a'];
+ expect(Object.keys(discussion.notes).length).toBe(2);
+ });
+ });
+
+ describe('Get note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('gets note by ID', () => {
+ const note = CommentsStore.get('a', 1);
+ expect(note).toBeDefined();
+ expect(note.id).toBe(1);
+ });
+ });
+
+ describe('Delete discussion', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('deletes discussion by ID', () => {
+ CommentsStore.delete('a', 1);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+
+ it('deletes discussion when no more notes', () => {
+ createDiscussion();
+ createDiscussion(2);
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+
+ CommentsStore.delete('a', 1);
+ CommentsStore.delete('a', 2);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+ });
+
+ describe('Update note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('updates note to be unresolved', () => {
+ CommentsStore.update('a', 1, false, 'test');
+
+ const note = CommentsStore.get('a', 1);
+ expect(note.resolved).toBe(false);
+ });
+ });
+
+ describe('Discussion resolved', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('is resolved with single note', () => {
+ const discussion = CommentsStore.state['a'];
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('is unresolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+
+ expect(discussion.isResolved()).toBe(false);
+ });
+
+ it('is resolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('resolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+
+ discussion.resolveAllNotes();
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('unresolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ discussion.unResolveAllNotes();
+ expect(discussion.isResolved()).toBe(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
deleted file mode 100644
index f956394ef53..00000000000
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ /dev/null
@@ -1,124 +0,0 @@
-/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
-/* global CommentsStore */
-
-require('~/diff_notes/models/discussion');
-require('~/diff_notes/models/note');
-require('~/diff_notes/stores/comments');
-
-(() => {
- function createDiscussion(noteId = 1, resolved = true) {
- CommentsStore.create('a', noteId, true, resolved, 'test');
- }
-
- beforeEach(() => {
- CommentsStore.state = {};
- });
-
- describe('New discussion', () => {
- it('creates new discussion', () => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- });
-
- it('creates new note in discussion', () => {
- createDiscussion();
- createDiscussion(2);
-
- const discussion = CommentsStore.state['a'];
- expect(Object.keys(discussion.notes).length).toBe(2);
- });
- });
-
- describe('Get note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('gets note by ID', () => {
- const note = CommentsStore.get('a', 1);
- expect(note).toBeDefined();
- expect(note.id).toBe(1);
- });
- });
-
- describe('Delete discussion', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('deletes discussion by ID', () => {
- CommentsStore.delete('a', 1);
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
-
- it('deletes discussion when no more notes', () => {
- createDiscussion();
- createDiscussion(2);
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
-
- CommentsStore.delete('a', 1);
- CommentsStore.delete('a', 2);
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
- });
-
- describe('Update note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('updates note to be unresolved', () => {
- CommentsStore.update('a', 1, false, 'test');
-
- const note = CommentsStore.get('a', 1);
- expect(note.resolved).toBe(false);
- });
- });
-
- describe('Discussion resolved', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('is resolved with single note', () => {
- const discussion = CommentsStore.state['a'];
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
-
- expect(discussion.isResolved()).toBe(false);
- });
-
- it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('resolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
-
- discussion.resolveAllNotes();
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('unresolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
-
- discussion.unResolveAllNotes();
- expect(discussion.isResolved()).toBe(false);
- });
- });
-})();
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
new file mode 100644
index 00000000000..85b73f1d4e2
--- /dev/null
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import actionsComp from '~/environments/components/environment_actions';
+
+describe('Actions Component', () => {
+ let ActionsComponent;
+ let actionsMock;
+ let spy;
+ let component;
+
+ beforeEach(() => {
+ ActionsComponent = Vue.extend(actionsComp);
+
+ actionsMock = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ ];
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+ component = new ActionsComponent({
+ propsData: {
+ actions: actionsMock,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(actionsMock.length);
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.querySelector('.dropdown').click();
+ component.$el.querySelector('.js-manual-action-link').click();
+
+ expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path);
+ });
+});
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
deleted file mode 100644
index 850586f9f3a..00000000000
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ /dev/null
@@ -1,66 +0,0 @@
-const ActionsComponent = require('~/environments/components/environment_actions');
-
-describe('Actions Component', () => {
- preloadFixtures('static/environments/element.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
-
- it('should render a dropdown with the provided actions', () => {
- const actionsMock = [
- {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- },
- {
- name: 'foo',
- play_path: '#',
- },
- ];
-
- const component = new ActionsComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- actions: actionsMock,
- playIconSvg: '<svg></svg>',
- },
- });
-
- expect(
- component.$el.querySelectorAll('.dropdown-menu li').length,
- ).toEqual(actionsMock.length);
- expect(
- component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
- ).toEqual(actionsMock[0].play_path);
- });
-
- it('should render a dropdown with the provided svg', () => {
- const actionsMock = [
- {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- },
- {
- name: 'foo',
- play_path: '#',
- },
- ];
-
- const component = new ActionsComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- actions: actionsMock,
- playIconSvg: '<svg></svg>',
- },
- });
-
- expect(
- component.$el.querySelector('.js-dropdown-play-icon-container').children,
- ).toContain('svg');
-
- expect(
- component.$el.querySelector('.js-action-play-icon-container').children,
- ).toContain('svg');
- });
-});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js
new file mode 100644
index 00000000000..9af218a27ff
--- /dev/null
+++ b/spec/javascripts/environments/environment_external_url_spec.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import externalUrlComp from '~/environments/components/environment_external_url';
+
+describe('External URL Component', () => {
+ let ExternalUrlComponent;
+
+ beforeEach(() => {
+ ExternalUrlComponent = Vue.extend(externalUrlComp);
+ });
+
+ it('should link to the provided externalUrl prop', () => {
+ const externalURL = 'https://gitlab.com';
+ const component = new ExternalUrlComponent({
+ propsData: {
+ externalUrl: externalURL,
+ },
+ }).$mount();
+
+ expect(component.$el.getAttribute('href')).toEqual(externalURL);
+ expect(component.$el.querySelector('fa-external-link')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
deleted file mode 100644
index 393dbb5aae0..00000000000
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ /dev/null
@@ -1,21 +0,0 @@
-const ExternalUrlComponent = require('~/environments/components/environment_external_url');
-
-describe('External URL Component', () => {
- preloadFixtures('static/environments/element.html.raw');
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
-
- it('should link to the provided externalUrl prop', () => {
- const externalURL = 'https://gitlab.com';
- const component = new ExternalUrlComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- externalUrl: externalURL,
- },
- });
-
- expect(component.$el.getAttribute('href')).toEqual(externalURL);
- expect(component.$el.querySelector('fa-external-link')).toBeDefined();
- });
-});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
new file mode 100644
index 00000000000..4d42de4d549
--- /dev/null
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -0,0 +1,212 @@
+import 'timeago.js';
+import Vue from 'vue';
+import environmentItemComp from '~/environments/components/environment_item';
+
+describe('Environment item', () => {
+ let EnvironmentItem;
+
+ beforeEach(() => {
+ EnvironmentItem = Vue.extend(environmentItemComp);
+ });
+
+ describe('When item is folder', () => {
+ let mockItem;
+ let component;
+
+ beforeEach(() => {
+ mockItem = {
+ name: 'review',
+ folderName: 'review',
+ size: 3,
+ isFolder: true,
+ environment_path: 'url',
+ };
+
+ component = new EnvironmentItem({
+ propsData: {
+ model: mockItem,
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ service: {},
+ },
+ }).$mount();
+ });
+
+ it('Should render folder icon and name', () => {
+ expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
+ expect(component.$el.querySelector('.folder-icon')).toBeDefined();
+ });
+
+ it('Should render the number of children in a badge', () => {
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
+ });
+ });
+
+ describe('when item is not folder', () => {
+ let environment;
+ let component;
+
+ beforeEach(() => {
+ environment = {
+ name: 'production',
+ size: 1,
+ state: 'stopped',
+ external_url: 'http://external.com',
+ environment_type: null,
+ last_deployment: {
+ id: 66,
+ iid: 6,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_path: 'root/ci-folders/tree/master',
+ },
+ tag: true,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1279,
+ name: 'deploy',
+ build_path: '/root/ci-folders/builds/1279',
+ retry_path: '/root/ci-folders/builds/1279/retry',
+ created_at: '2016-11-29T18:11:58.430Z',
+ updated_at: '2016-11-29T18:11:58.430Z',
+ },
+ manual_actions: [
+ {
+ name: 'action',
+ play_path: '/play',
+ },
+ ],
+ },
+ 'stop_action?': true,
+ environment_path: 'root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ };
+
+ component = new EnvironmentItem({
+ propsData: {
+ model: environment,
+ canCreateDeployment: true,
+ canReadEnvironment: true,
+ service: {},
+ },
+ }).$mount();
+ });
+
+ it('should render environment name', () => {
+ expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
+ });
+
+ describe('With deployment', () => {
+ it('should render deployment internal id', () => {
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent,
+ ).toContain(environment.last_deployment.iid);
+
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent,
+ ).toContain('#');
+ });
+
+ it('should render last deployment date', () => {
+ const timeagoInstance = new timeago(); // eslint-disable-line
+ const formatedDate = timeagoInstance.format(
+ environment.last_deployment.deployable.created_at,
+ );
+
+ expect(
+ component.$el.querySelector('.environment-created-date-timeago').textContent,
+ ).toContain(formatedDate);
+ });
+
+ describe('With user information', () => {
+ it('should render user avatar with link to profile', () => {
+ expect(
+ component.$el.querySelector('.js-deploy-user-container').getAttribute('href'),
+ ).toEqual(environment.last_deployment.user.web_url);
+ });
+ });
+
+ describe('With build url', () => {
+ it('Should link to build url provided', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href'),
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+
+ it('Should render deployable name and id', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href'),
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+ });
+
+ describe('With commit information', () => {
+ it('should render commit component', () => {
+ expect(
+ component.$el.querySelector('.js-commit-component'),
+ ).toBeDefined();
+ });
+ });
+ });
+
+ describe('With manual actions', () => {
+ it('Should render actions component', () => {
+ expect(
+ component.$el.querySelector('.js-manual-actions-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With external URL', () => {
+ it('should render external url component', () => {
+ expect(
+ component.$el.querySelector('.js-external-url-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With stop action', () => {
+ it('Should render stop action component', () => {
+ expect(
+ component.$el.querySelector('.js-stop-component-container'),
+ ).toBeDefined();
+ });
+ });
+
+ describe('With retry action', () => {
+ it('Should render rollback component', () => {
+ expect(
+ component.$el.querySelector('.js-rollback-component-container'),
+ ).toBeDefined();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
deleted file mode 100644
index 7fea80ed799..00000000000
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ /dev/null
@@ -1,210 +0,0 @@
-window.timeago = require('timeago.js');
-const EnvironmentItem = require('~/environments/components/environment_item');
-
-describe('Environment item', () => {
- preloadFixtures('static/environments/table.html.raw');
- beforeEach(() => {
- loadFixtures('static/environments/table.html.raw');
- });
-
- describe('When item is folder', () => {
- let mockItem;
- let component;
-
- beforeEach(() => {
- mockItem = {
- name: 'review',
- folderName: 'review',
- size: 3,
- isFolder: true,
- environment_path: 'url',
- };
-
- component = new EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
- propsData: {
- model: mockItem,
- canCreateDeployment: false,
- canReadEnvironment: true,
- },
- });
- });
-
- it('Should render folder icon and name', () => {
- expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
- expect(component.$el.querySelector('.folder-icon')).toBeDefined();
- });
-
- it('Should render the number of children in a badge', () => {
- expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
- });
- });
-
- describe('when item is not folder', () => {
- let environment;
- let component;
-
- beforeEach(() => {
- environment = {
- name: 'production',
- size: 1,
- state: 'stopped',
- external_url: 'http://external.com',
- environment_type: null,
- last_deployment: {
- id: 66,
- iid: 6,
- sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- ref: {
- name: 'master',
- ref_path: 'root/ci-folders/tree/master',
- },
- tag: true,
- 'last?': true,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit: {
- id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- short_id: '500aabcb',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2016-11-07T18:28:13.000+00:00',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- },
- deployable: {
- id: 1279,
- name: 'deploy',
- build_path: '/root/ci-folders/builds/1279',
- retry_path: '/root/ci-folders/builds/1279/retry',
- created_at: '2016-11-29T18:11:58.430Z',
- updated_at: '2016-11-29T18:11:58.430Z',
- },
- manual_actions: [
- {
- name: 'action',
- play_path: '/play',
- },
- ],
- },
- 'stop_action?': true,
- environment_path: 'root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-10T15:55:58.778Z',
- };
-
- component = new EnvironmentItem({
- el: document.querySelector('tr#environment-row'),
- propsData: {
- model: environment,
- canCreateDeployment: true,
- canReadEnvironment: true,
- },
- });
- });
-
- it('should render environment name', () => {
- expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
- });
-
- describe('With deployment', () => {
- it('should render deployment internal id', () => {
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain(environment.last_deployment.iid);
-
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain('#');
- });
-
- it('should render last deployment date', () => {
- const timeagoInstance = new timeago(); // eslint-disable-line
- const formatedDate = timeagoInstance.format(
- environment.last_deployment.deployable.created_at,
- );
-
- expect(
- component.$el.querySelector('.environment-created-date-timeago').textContent,
- ).toContain(formatedDate);
- });
-
- describe('With user information', () => {
- it('should render user avatar with link to profile', () => {
- expect(
- component.$el.querySelector('.js-deploy-user-container').getAttribute('href'),
- ).toEqual(environment.last_deployment.user.web_url);
- });
- });
-
- describe('With build url', () => {
- it('Should link to build url provided', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
- });
-
- it('Should render deployable name and id', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
- });
- });
-
- describe('With commit information', () => {
- it('should render commit component', () => {
- expect(
- component.$el.querySelector('.js-commit-component'),
- ).toBeDefined();
- });
- });
- });
-
- describe('With manual actions', () => {
- it('Should render actions component', () => {
- expect(
- component.$el.querySelector('.js-manual-actions-container'),
- ).toBeDefined();
- });
- });
-
- describe('With external URL', () => {
- it('should render external url component', () => {
- expect(
- component.$el.querySelector('.js-external-url-container'),
- ).toBeDefined();
- });
- });
-
- describe('With stop action', () => {
- it('Should render stop action component', () => {
- expect(
- component.$el.querySelector('.js-stop-component-container'),
- ).toBeDefined();
- });
- });
-
- describe('With retry action', () => {
- it('Should render rollback component', () => {
- expect(
- component.$el.querySelector('.js-rollback-component-container'),
- ).toBeDefined();
- });
- });
- });
-});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
new file mode 100644
index 00000000000..7cb39d9df03
--- /dev/null
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -0,0 +1,59 @@
+import Vue from 'vue';
+import rollbackComp from '~/environments/components/environment_rollback';
+
+describe('Rollback Component', () => {
+ const retryURL = 'https://gitlab.com/retry';
+ let RollbackComponent;
+ let spy;
+
+ beforeEach(() => {
+ RollbackComponent = Vue.extend(rollbackComp);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+ });
+
+ it('Should render Re-deploy label when isLastDeployment is true', () => {
+ const component = new RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: true,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+ });
+
+ it('Should render Rollback label when isLastDeployment is false', () => {
+ const component = new RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: false,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+ });
+
+ it('should call the service when the button is clicked', () => {
+ const component = new RollbackComponent({
+ propsData: {
+ retryUrl: retryURL,
+ isLastDeployment: false,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.click();
+
+ expect(spy).toHaveBeenCalledWith(retryURL);
+ });
+});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
deleted file mode 100644
index 4a596baad09..00000000000
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ /dev/null
@@ -1,47 +0,0 @@
-const RollbackComponent = require('~/environments/components/environment_rollback');
-
-describe('Rollback Component', () => {
- preloadFixtures('static/environments/element.html.raw');
-
- const retryURL = 'https://gitlab.com/retry';
-
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
-
- it('Should link to the provided retryUrl', () => {
- const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- retryUrl: retryURL,
- isLastDeployment: true,
- },
- });
-
- expect(component.$el.getAttribute('href')).toEqual(retryURL);
- });
-
- it('Should render Re-deploy label when isLastDeployment is true', () => {
- const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- retryUrl: retryURL,
- isLastDeployment: true,
- },
- });
-
- expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
- });
-
- it('Should render Rollback label when isLastDeployment is false', () => {
- const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- retryUrl: retryURL,
- isLastDeployment: false,
- },
- });
-
- expect(component.$el.querySelector('span').textContent).toContain('Rollback');
- });
-});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
new file mode 100644
index 00000000000..9601575577e
--- /dev/null
+++ b/spec/javascripts/environments/environment_spec.js
@@ -0,0 +1,178 @@
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsComponent from '~/environments/components/environment';
+import { environment } from './mock_data';
+
+describe('Environment', () => {
+ preloadFixtures('static/environments/environments.html.raw');
+
+ let component;
+
+ beforeEach(() => {
+ loadFixtures('static/environments/environments.html.raw');
+ });
+
+ describe('successfull request', () => {
+ describe('without environments', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ component = new EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-new-environment-button').textContent,
+ ).toContain('New Environment');
+
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with paginated environments', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ }), {
+ status: 200,
+ headers: {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ component = new EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should render a table with environments', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(1);
+ done();
+ }, 0);
+ });
+
+ describe('pagination', () => {
+ it('should render pagination', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
+ done();
+ }, 0);
+ });
+
+ it('should update url when no search params are present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page is already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?scope=all&page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present and page is first param', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1&scope=all');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ component = new EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
deleted file mode 100644
index edd0cad32d0..00000000000
--- a/spec/javascripts/environments/environment_spec.js.es6
+++ /dev/null
@@ -1,178 +0,0 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsComponent = require('~/environments/components/environment');
-const { environment } = require('./mock_data');
-
-describe('Environment', () => {
- preloadFixtures('static/environments/environments.html.raw');
-
- let component;
-
- beforeEach(() => {
- loadFixtures('static/environments/environments.html.raw');
- });
-
- describe('successfull request', () => {
- describe('without environments', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- });
-
- it('should render the empty state', (done) => {
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-new-environment-button').textContent,
- ).toContain('New Environment');
-
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
-
- done();
- }, 0);
- });
- });
-
- describe('with paginated environments', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [environment],
- stopped_count: 1,
- available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- });
-
- it('should render a table with environments', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(1);
- done();
- }, 0);
- });
-
- describe('pagination', () => {
- it('should render pagination', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
- done();
- }, 0);
- });
-
- it('should update url when no search params are present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page is already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?scope=all&page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present and page is first param', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1&scope=all');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
- done();
- }, 0);
- });
- });
- });
- });
-
- describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
- });
-
- it('should render empty state', (done) => {
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
new file mode 100644
index 00000000000..8f79b88f3df
--- /dev/null
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import stopComp from '~/environments/components/environment_stop';
+
+describe('Stop Component', () => {
+ let StopComponent;
+ let component;
+ let spy;
+ const stopURL = '/stop';
+
+ beforeEach(() => {
+ StopComponent = Vue.extend(stopComp);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+ spyOn(window, 'confirm').and.returnValue(true);
+
+ component = new StopComponent({
+ propsData: {
+ stopUrl: stopURL,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a button to stop the environment', () => {
+ expect(component.$el.tagName).toEqual('BUTTON');
+ expect(component.$el.getAttribute('title')).toEqual('Stop Environment');
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.click();
+ expect(spy).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
deleted file mode 100644
index 5ca65b1debc..00000000000
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ /dev/null
@@ -1,28 +0,0 @@
-const StopComponent = require('~/environments/components/environment_stop');
-
-describe('Stop Component', () => {
- preloadFixtures('static/environments/element.html.raw');
-
- let stopURL;
- let component;
-
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
-
- stopURL = '/stop';
- component = new StopComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- stopUrl: stopURL,
- },
- });
- });
-
- it('should link to the provided URL', () => {
- expect(component.$el.getAttribute('href')).toEqual(stopURL);
- });
-
- it('should have a data-confirm attribute', () => {
- expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
- });
-});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
new file mode 100644
index 00000000000..3df967848a7
--- /dev/null
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import environmentTableComp from '~/environments/components/environments_table';
+
+describe('Environment item', () => {
+ preloadFixtures('static/environments/element.html.raw');
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+ });
+
+ it('Should render a table', () => {
+ const mockItem = {
+ name: 'review',
+ size: 3,
+ isFolder: true,
+ latest: {
+ environment_path: 'url',
+ },
+ };
+
+ const EnvironmentTable = Vue.extend(environmentTableComp);
+
+ const component = new EnvironmentTable({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ environments: [{ mockItem }],
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ service: {},
+ },
+ }).$mount();
+
+ expect(component.$el.tagName).toEqual('TABLE');
+ });
+});
diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6
deleted file mode 100644
index be4330b5012..00000000000
--- a/spec/javascripts/environments/environment_table_spec.js.es6
+++ /dev/null
@@ -1,30 +0,0 @@
-const EnvironmentTable = require('~/environments/components/environments_table');
-
-describe('Environment item', () => {
- preloadFixtures('static/environments/element.html.raw');
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
-
- it('Should render a table', () => {
- const mockItem = {
- name: 'review',
- size: 3,
- isFolder: true,
- latest: {
- environment_path: 'url',
- },
- };
-
- const component = new EnvironmentTable({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- environments: [{ mockItem }],
- canCreateDeployment: false,
- canReadEnvironment: true,
- },
- });
-
- expect(component.$el.tagName).toEqual('TABLE');
- });
-});
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
new file mode 100644
index 00000000000..b07aa4e1745
--- /dev/null
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import terminalComp from '~/environments/components/environment_terminal_button';
+
+describe('Stop Component', () => {
+ let TerminalComponent;
+ let component;
+ const terminalPath = '/path';
+
+ beforeEach(() => {
+ TerminalComponent = Vue.extend(terminalComp);
+
+ component = new TerminalComponent({
+ propsData: {
+ terminalPath,
+ },
+ }).$mount();
+ });
+
+ it('should render a link to open a web terminal with the provided path', () => {
+ expect(component.$el.tagName).toEqual('A');
+ expect(component.$el.getAttribute('title')).toEqual('Open web terminal');
+ expect(component.$el.getAttribute('href')).toEqual(terminalPath);
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
new file mode 100644
index 00000000000..115d84b50f5
--- /dev/null
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -0,0 +1,58 @@
+import Store from '~/environments/stores/environments_store';
+import { environmentsList, serverData } from './mock_data';
+
+(() => {
+ describe('Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new Store();
+ });
+
+ it('should start with a blank state', () => {
+ expect(store.state.environments.length).toEqual(0);
+ expect(store.state.stoppedCounter).toEqual(0);
+ expect(store.state.availableCounter).toEqual(0);
+ expect(store.state.paginationInformation).toEqual({});
+ });
+
+ it('should store environments', () => {
+ store.storeEnvironments(serverData);
+ expect(store.state.environments.length).toEqual(serverData.length);
+ expect(store.state.environments[0]).toEqual(environmentsList[0]);
+ });
+
+ it('should store available count', () => {
+ store.storeAvailableCount(2);
+ expect(store.state.availableCounter).toEqual(2);
+ });
+
+ it('should store stopped count', () => {
+ store.storeStoppedCount(2);
+ expect(store.state.stoppedCounter).toEqual(2);
+ });
+
+ it('should store pagination information', () => {
+ const pagination = {
+ 'X-nExt-pAge': '2',
+ 'X-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '2',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ };
+
+ const expectedResult = {
+ perPage: 1,
+ page: 1,
+ total: 37,
+ totalPages: 2,
+ nextPage: 2,
+ previousPage: 2,
+ };
+
+ store.setPagination(pagination);
+ expect(store.state.paginationInformation).toEqual(expectedResult);
+ });
+ });
+})();
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
deleted file mode 100644
index 77e182b3830..00000000000
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ /dev/null
@@ -1,58 +0,0 @@
-const Store = require('~/environments/stores/environments_store');
-const { environmentsList, serverData } = require('./mock_data');
-
-(() => {
- describe('Store', () => {
- let store;
-
- beforeEach(() => {
- store = new Store();
- });
-
- it('should start with a blank state', () => {
- expect(store.state.environments.length).toEqual(0);
- expect(store.state.stoppedCounter).toEqual(0);
- expect(store.state.availableCounter).toEqual(0);
- expect(store.state.paginationInformation).toEqual({});
- });
-
- it('should store environments', () => {
- store.storeEnvironments(serverData);
- expect(store.state.environments.length).toEqual(serverData.length);
- expect(store.state.environments[0]).toEqual(environmentsList[0]);
- });
-
- it('should store available count', () => {
- store.storeAvailableCount(2);
- expect(store.state.availableCounter).toEqual(2);
- });
-
- it('should store stopped count', () => {
- store.storeStoppedCount(2);
- expect(store.state.stoppedCounter).toEqual(2);
- });
-
- it('should store pagination information', () => {
- const pagination = {
- 'X-nExt-pAge': '2',
- 'X-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '2',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- };
-
- const expectedResult = {
- perPage: 1,
- page: 1,
- total: 37,
- totalPages: 2,
- nextPage: 2,
- previousPage: 2,
- };
-
- store.setPagination(pagination);
- expect(store.state.paginationInformation).toEqual(expectedResult);
- });
- });
-})();
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
new file mode 100644
index 00000000000..43a217a67f5
--- /dev/null
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -0,0 +1,202 @@
+import Vue from 'vue';
+import '~/flash';
+import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view';
+import { environmentsList } from '../mock_data';
+
+describe('Environments Folder View', () => {
+ preloadFixtures('static/environments/environments_folder_view.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/environments_folder_view.html.raw');
+ window.history.pushState({}, null, 'environments/folders/build');
+ });
+
+ let component;
+
+ describe('successfull request', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: environmentsList,
+ stopped_count: 1,
+ available_count: 0,
+ }), {
+ status: 200,
+ headers: {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ component = new EnvironmentsFolderViewComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should render a table with environments', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(2);
+ done();
+ }, 0);
+ });
+
+ it('should render available tab with count', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ ).toContain('Available');
+
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+
+ it('should render stopped tab with count', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ ).toContain('Stopped');
+
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ ).toContain('1');
+ done();
+ }, 0);
+ });
+
+ it('should render parent folder name', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-folder-name').textContent,
+ ).toContain('Environments / build');
+ done();
+ }, 0);
+ });
+
+ describe('pagination', () => {
+ it('should render pagination', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
+ done();
+ }, 0);
+ });
+
+ it('should update url when no search params are present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page is already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?scope=all&page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present and page is first param', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1&scope=all');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should not render a table', (done) => {
+ component = new EnvironmentsFolderViewComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('table'),
+ ).toBe(null);
+ done();
+ }, 0);
+ });
+
+ it('should render available tab with count 0', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ ).toContain('Available');
+
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+
+ it('should render stopped tab with count 0', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ ).toContain('Stopped');
+
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
deleted file mode 100644
index d1335b5b304..00000000000
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
+++ /dev/null
@@ -1,202 +0,0 @@
-const Vue = require('vue');
-require('~/flash');
-const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
-const { environmentsList } = require('../mock_data');
-
-describe('Environments Folder View', () => {
- preloadFixtures('static/environments/environments_folder_view.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/environments/environments_folder_view.html.raw');
- window.history.pushState({}, null, 'environments/folders/build');
- });
-
- let component;
-
- describe('successfull request', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: environmentsList,
- stopped_count: 1,
- available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- component = new EnvironmentsFolderViewComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- });
-
- it('should render a table with environments', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('table tbody tr').length,
- ).toEqual(2);
- done();
- }, 0);
- });
-
- it('should render available tab with count', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-available-environments-folder-tab').textContent,
- ).toContain('Available');
-
- expect(
- component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
- ).toContain('0');
- done();
- }, 0);
- });
-
- it('should render stopped tab with count', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
- ).toContain('Stopped');
-
- expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
- ).toContain('1');
- done();
- }, 0);
- });
-
- it('should render parent folder name', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-folder-name').textContent,
- ).toContain('Environments / build');
- done();
- }, 0);
- });
-
- describe('pagination', () => {
- it('should render pagination', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
- done();
- }, 0);
- });
-
- it('should update url when no search params are present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page is already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?scope=all&page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present and page is first param', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1&scope=all');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
- done();
- }, 0);
- });
- });
- });
-
- describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
- });
-
- it('should not render a table', (done) => {
- component = new EnvironmentsFolderViewComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('table'),
- ).toBe(null);
- done();
- }, 0);
- });
-
- it('should render available tab with count 0', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-available-environments-folder-tab').textContent,
- ).toContain('Available');
-
- expect(
- component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
- ).toContain('0');
- done();
- }, 0);
- });
-
- it('should render stopped tab with count 0', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
- ).toContain('Stopped');
-
- expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
- ).toContain('0');
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js
new file mode 100644
index 00000000000..30861481cc5
--- /dev/null
+++ b/spec/javascripts/environments/mock_data.js
@@ -0,0 +1,86 @@
+export const environmentsList = [
+ {
+ name: 'DEV',
+ size: 1,
+ id: 7,
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
+ },
+ {
+ folderName: 'build',
+ size: 5,
+ id: 12,
+ name: 'build/update-README',
+ state: 'available',
+ external_url: null,
+ environment_type: 'build',
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/12',
+ stop_path: '/root/review-app/environments/12/stop',
+ created_at: '2017-02-01T19:42:18.400Z',
+ updated_at: '2017-02-01T19:42:18.400Z',
+ },
+];
+
+export const serverData = [
+ {
+ name: 'DEV',
+ size: 1,
+ latest: {
+ id: 7,
+ name: 'DEV',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
+ },
+ },
+ {
+ name: 'build',
+ size: 5,
+ latest: {
+ id: 12,
+ name: 'build/update-README',
+ state: 'available',
+ external_url: null,
+ environment_type: 'build',
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/12',
+ stop_path: '/root/review-app/environments/12/stop',
+ created_at: '2017-02-01T19:42:18.400Z',
+ updated_at: '2017-02-01T19:42:18.400Z',
+ },
+ },
+];
+
+export const environment = {
+ name: 'DEV',
+ size: 1,
+ latest: {
+ id: 7,
+ name: 'DEV',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
+ },
+};
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
deleted file mode 100644
index 5c395c6b2d8..00000000000
--- a/spec/javascripts/environments/mock_data.js.es6
+++ /dev/null
@@ -1,92 +0,0 @@
-const environmentsList = [
- {
- name: 'DEV',
- size: 1,
- id: 7,
- state: 'available',
- external_url: null,
- environment_type: null,
- last_deployment: null,
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/7',
- stop_path: '/root/review-app/environments/7/stop',
- created_at: '2017-01-31T10:53:46.894Z',
- updated_at: '2017-01-31T10:53:46.894Z',
- },
- {
- folderName: 'build',
- size: 5,
- id: 12,
- name: 'build/update-README',
- state: 'available',
- external_url: null,
- environment_type: 'build',
- last_deployment: null,
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/12',
- stop_path: '/root/review-app/environments/12/stop',
- created_at: '2017-02-01T19:42:18.400Z',
- updated_at: '2017-02-01T19:42:18.400Z',
- },
-];
-
-const serverData = [
- {
- name: 'DEV',
- size: 1,
- latest: {
- id: 7,
- name: 'DEV',
- state: 'available',
- external_url: null,
- environment_type: null,
- last_deployment: null,
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/7',
- stop_path: '/root/review-app/environments/7/stop',
- created_at: '2017-01-31T10:53:46.894Z',
- updated_at: '2017-01-31T10:53:46.894Z',
- },
- },
- {
- name: 'build',
- size: 5,
- latest: {
- id: 12,
- name: 'build/update-README',
- state: 'available',
- external_url: null,
- environment_type: 'build',
- last_deployment: null,
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/12',
- stop_path: '/root/review-app/environments/12/stop',
- created_at: '2017-02-01T19:42:18.400Z',
- updated_at: '2017-02-01T19:42:18.400Z',
- },
- },
-];
-
-const environment = {
- name: 'DEV',
- size: 1,
- latest: {
- id: 7,
- name: 'DEV',
- state: 'available',
- external_url: null,
- environment_type: null,
- last_deployment: null,
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/7',
- stop_path: '/root/review-app/environments/7/stop',
- created_at: '2017-01-31T10:53:46.894Z',
- updated_at: '2017-01-31T10:53:46.894Z',
- },
-};
-
-module.exports = {
- environmentsList,
- environment,
- serverData,
-};
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
new file mode 100644
index 00000000000..4b871fe967d
--- /dev/null
+++ b/spec/javascripts/extensions/array_spec.js
@@ -0,0 +1,22 @@
+/* eslint-disable space-before-function-paren, no-var */
+
+require('~/extensions/array');
+
+(function() {
+ describe('Array extensions', function() {
+ describe('first', function() {
+ return it('returns the first item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.first()).toBe(0);
+ });
+ });
+ describe('last', function() {
+ return it('returns the last item', function() {
+ var arr;
+ arr = [0, 1, 2, 3, 4, 5];
+ return expect(arr.last()).toBe(5);
+ });
+ });
+ });
+}).call(window);
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
deleted file mode 100644
index 60f6b9b78e3..00000000000
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable space-before-function-paren, no-var */
-
-require('~/extensions/array');
-
-(function() {
- describe('Array extensions', function() {
- describe('first', function() {
- return it('returns the first item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.first()).toBe(0);
- });
- });
- describe('last', function() {
- return it('returns the last item', function() {
- var arr;
- arr = [0, 1, 2, 3, 4, 5];
- return expect(arr.last()).toBe(5);
- });
- });
-
- describe('find', function () {
- beforeEach(() => {
- this.arr = [0, 1, 2, 3, 4, 5];
- });
-
- it('returns the item that first passes the predicate function', () => {
- expect(this.arr.find(item => item === 2)).toBe(2);
- });
-
- it('returns undefined if no items pass the predicate function', () => {
- expect(this.arr.find(item => item === 6)).not.toBeDefined();
- });
-
- it('error when called on undefined or null', () => {
- expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow();
- expect(Array.prototype.find.bind(null, item => item === 1)).toThrow();
- });
-
- it('error when predicate is not a function', () => {
- expect(Array.prototype.find.bind(this.arr, 1)).toThrow();
- });
- });
- });
-}).call(window);
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
deleted file mode 100644
index 2d8a128ed33..00000000000
--- a/spec/javascripts/extensions/element_spec.js.es6
+++ /dev/null
@@ -1,38 +0,0 @@
-require('~/extensions/element');
-
-(() => {
- describe('Element extensions', function () {
- beforeEach(() => {
- this.element = document.createElement('ul');
- });
-
- describe('matches', () => {
- it('returns true if element matches the selector', () => {
- expect(this.element.matches('ul')).toBeTruthy();
- });
-
- it("returns false if element doesn't match the selector", () => {
- expect(this.element.matches('.not-an-element')).toBeFalsy();
- });
- });
-
- describe('closest', () => {
- beforeEach(() => {
- this.childElement = document.createElement('li');
- this.element.appendChild(this.childElement);
- });
-
- it('returns the closest parent that matches the selector', () => {
- expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
- });
-
- it('returns itself if it matches the selector', () => {
- expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
- });
-
- it('returns undefined if nothing matches the selector', () => {
- expect(this.childElement.closest('.no-an-element')).toBeFalsy();
- });
- });
- });
-})();
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
deleted file mode 100644
index 096d3272eac..00000000000
--- a/spec/javascripts/extensions/jquery_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable space-before-function-paren, no-var */
-
-require('~/extensions/jquery');
-
-(function() {
- describe('jQuery extensions', function() {
- describe('disable', function() {
- beforeEach(function() {
- return setFixtures('<input type="text" />');
- });
- it('adds the disabled attribute', function() {
- var $input;
- $input = $('input').first();
- $input.disable();
- return expect($input).toHaveAttr('disabled', 'disabled');
- });
- return it('adds the disabled class', function() {
- var $input;
- $input = $('input').first();
- $input.disable();
- return expect($input).toHaveClass('disabled');
- });
- });
- return describe('enable', function() {
- beforeEach(function() {
- return setFixtures('<input type="text" disabled="disabled" class="disabled" />');
- });
- it('removes the disabled attribute', function() {
- var $input;
- $input = $('input').first();
- $input.enable();
- return expect($input).not.toHaveAttr('disabled');
- });
- return it('removes the disabled class', function() {
- var $input;
- $input = $('input').first();
- $input.enable();
- return expect($input).not.toHaveClass('disabled');
- });
- });
- });
-}).call(window);
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
deleted file mode 100644
index 2467ed78459..00000000000
--- a/spec/javascripts/extensions/object_spec.js.es6
+++ /dev/null
@@ -1,25 +0,0 @@
-require('~/extensions/object');
-
-describe('Object extensions', () => {
- describe('assign', () => {
- it('merges source object into target object', () => {
- const targetObj = {};
- const sourceObj = {
- foo: 'bar',
- };
- Object.assign(targetObj, sourceObj);
- expect(targetObj.foo).toBe('bar');
- });
-
- it('merges object with the same properties', () => {
- const targetObj = {
- foo: 'bar',
- };
- const sourceObj = {
- foo: 'baz',
- };
- Object.assign(targetObj, sourceObj);
- expect(targetObj.foo).toBe('baz');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
new file mode 100644
index 00000000000..c16f77c53a2
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -0,0 +1,71 @@
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown');
+require('~/filtered_search/dropdown_user');
+
+(() => {
+ describe('Dropdown User', () => {
+ describe('getSearchInput', () => {
+ let dropdownUser;
+
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
+
+ dropdownUser = new gl.DropdownUser();
+ });
+
+ it('should not return the double quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: '"johnny appleseed',
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ });
+
+ it('should not return the single quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: '\'larry boy',
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('larry boy');
+ });
+ });
+
+ describe('config droplabAjaxFilter\'s endpoint', () => {
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ });
+
+ it('should return endpoint', () => {
+ window.gon = {
+ relative_url_root: '',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint when relative_url_root is undefined', () => {
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint with relative url when available', () => {
+ window.gon = {
+ relative_url_root: '/gitlab_directory',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
+ });
+
+ afterEach(() => {
+ window.gon = {};
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
deleted file mode 100644
index fa9d03c8a9a..00000000000
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
+++ /dev/null
@@ -1,75 +0,0 @@
-require('~/filtered_search/dropdown_utils');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown');
-require('~/filtered_search/dropdown_user');
-
-(() => {
- describe('Dropdown User', () => {
- describe('getSearchInput', () => {
- let dropdownUser;
-
- beforeEach(() => {
- spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
- spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
-
- dropdownUser = new gl.DropdownUser();
- });
-
- it('should not return the double quote found in value', () => {
- spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: {
- value: '"johnny appleseed',
- },
- });
-
- expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
- });
-
- it('should not return the single quote found in value', () => {
- spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: {
- value: '\'larry boy',
- },
- });
-
- expect(dropdownUser.getSearchInput()).toBe('larry boy');
- });
- });
-
- describe('config droplabAjaxFilter\'s endpoint', () => {
- beforeEach(() => {
- spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
- });
-
- it('should return endpoint', () => {
- window.gon = {
- relative_url_root: '',
- };
- const dropdown = new gl.DropdownUser();
-
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
- });
-
- it('should return endpoint when relative_url_root is undefined', () => {
- const dropdown = new gl.DropdownUser();
-
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
- });
-
- it('should return endpoint with relative url when available', () => {
- window.gon = {
- relative_url_root: '/gitlab_directory',
- };
- const dropdown = new gl.DropdownUser();
-
- expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
- });
-
- afterEach(() => {
- window.gon = {};
- });
- });
- });
-})();
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
new file mode 100644
index 00000000000..e6538020896
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -0,0 +1,310 @@
+require('~/extensions/array');
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+
+(() => {
+ describe('Dropdown Utils', () => {
+ describe('getEscapedText', () => {
+ it('should return same word when it has no space', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
+ expect(escaped).toBe('textWithoutSpace');
+ });
+
+ it('should escape with double quotes', () => {
+ let escaped = gl.DropdownUtils.getEscapedText('text with space');
+ expect(escaped).toBe('"text with space"');
+
+ escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
+ expect(escaped).toBe('"won\'t fix"');
+ });
+
+ it('should escape with single quotes', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
+ expect(escaped).toBe('\'won"t fix\'');
+ });
+
+ it('should escape with single quotes by default', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
+ expect(escaped).toBe('\'won"t\' fix\'');
+ });
+ });
+
+ describe('filterWithSymbol', () => {
+ let input;
+ const item = {
+ title: '@root',
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter without symbol', () => {
+ input.value = 'roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with symbol', () => {
+ input.value = '@roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ describe('filters multiple word title', () => {
+ const multipleWordItem = {
+ title: 'Community Contributions',
+ };
+
+ it('should filter with double quote', () => {
+ input.value = '"';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and symbol', () => {
+ input.value = '~"';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and multiple words', () => {
+ input.value = '"community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote, symbol and multiple words', () => {
+ input.value = '~"community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote', () => {
+ input.value = '\'';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and symbol', () => {
+ input.value = '~\'';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and multiple words', () => {
+ input.value = '\'community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote, symbol and multiple words', () => {
+ input.value = '~\'community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+ });
+ });
+
+ describe('filterHint', () => {
+ let input;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search" type="text" id="test" />
+ </li>
+ </ul>
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter', () => {
+ input.value = 'l';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ });
+ expect(updatedItem.droplab_hidden).toBe(false);
+
+ input.value = 'o';
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ });
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+
+ it('should return droplab_hidden false when item has no hint', () => {
+ const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should allow multiple if item.type is array', () => {
+ input.value = 'label:~first la';
+ const updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ type: 'array',
+ });
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should prevent multiple if item.type is not array', () => {
+ input.value = 'milestone:~first mile';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
+ });
+ expect(updatedItem.droplab_hidden).toBe(true);
+
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
+ type: 'string',
+ });
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+ });
+
+ describe('setDataValueIfSelected', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
+ .and.callFake(() => {});
+ });
+
+ it('calls addWordToInput when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
+ });
+
+ it('returns true when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(true);
+ });
+
+ it('returns false when dataValue does not exist', () => {
+ const selected = {
+ getAttribute: () => null,
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('getInputSelectionPosition', () => {
+ describe('word with trailing spaces', () => {
+ const value = 'label:none ';
+
+ it('should return selectionStart when cursor is at the trailing space', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 11,
+ value,
+ });
+
+ expect(left).toBe(11);
+ expect(right).toBe(11);
+ });
+
+ it('should return input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the middle of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 7,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 10,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+ });
+
+ describe('multiple words', () => {
+ const value = 'label:~"Community Contribution"';
+
+ it('should return input when cursor is after the first word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 17,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+
+ it('should return input when cursor is before the second word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 18,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+ });
+
+ describe('incomplete multiple words', () => {
+ const value = 'label:~"Community Contribution';
+
+ it('should return entire input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+
+ it('should return entire input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 30,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
deleted file mode 100644
index 1e2d7582d5b..00000000000
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
+++ /dev/null
@@ -1,290 +0,0 @@
-require('~/extensions/array');
-require('~/filtered_search/dropdown_utils');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown_manager');
-
-(() => {
- describe('Dropdown Utils', () => {
- describe('getEscapedText', () => {
- it('should return same word when it has no space', () => {
- const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
- expect(escaped).toBe('textWithoutSpace');
- });
-
- it('should escape with double quotes', () => {
- let escaped = gl.DropdownUtils.getEscapedText('text with space');
- expect(escaped).toBe('"text with space"');
-
- escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
- expect(escaped).toBe('"won\'t fix"');
- });
-
- it('should escape with single quotes', () => {
- const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
- expect(escaped).toBe('\'won"t fix\'');
- });
-
- it('should escape with single quotes by default', () => {
- const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
- expect(escaped).toBe('\'won"t\' fix\'');
- });
- });
-
- describe('filterWithSymbol', () => {
- let input;
- const item = {
- title: '@root',
- };
-
- beforeEach(() => {
- setFixtures(`
- <input type="text" id="test" />
- `);
-
- input = document.getElementById('test');
- });
-
- it('should filter without symbol', () => {
- input.value = ':roo';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with symbol', () => {
- input.value = '@roo';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with colon', () => {
- input.value = 'roo';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- describe('filters multiple word title', () => {
- const multipleWordItem = {
- title: 'Community Contributions',
- };
-
- it('should filter with double quote', () => {
- input.value = 'label:"';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote and symbol', () => {
- input.value = 'label:~"';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote and multiple words', () => {
- input.value = 'label:"community con';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote, symbol and multiple words', () => {
- input.value = 'label:~"community con';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote', () => {
- input.value = 'label:\'';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote and symbol', () => {
- input.value = 'label:~\'';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote and multiple words', () => {
- input.value = 'label:\'community con';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote, symbol and multiple words', () => {
- input.value = 'label:~\'community con';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
- });
- });
-
- describe('filterHint', () => {
- let input;
-
- beforeEach(() => {
- setFixtures(`
- <input type="text" id="test" />
- `);
-
- input = document.getElementById('test');
- });
-
- it('should filter', () => {
- input.value = 'l';
- let updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'label',
- });
- expect(updatedItem.droplab_hidden).toBe(false);
-
- input.value = 'o';
- updatedItem = gl.DropdownUtils.filterHint(input, {
- hint: 'label',
- }, 'o');
- expect(updatedItem.droplab_hidden).toBe(true);
- });
-
- it('should return droplab_hidden false when item has no hint', () => {
- const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
- expect(updatedItem.droplab_hidden).toBe(false);
- });
- });
-
- describe('setDataValueIfSelected', () => {
- beforeEach(() => {
- spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
- .and.callFake(() => {});
- });
-
- it('calls addWordToInput when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- };
-
- gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
- });
-
- it('returns true when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- };
-
- const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(result).toBe(true);
- });
-
- it('returns false when dataValue does not exist', () => {
- const selected = {
- getAttribute: () => null,
- };
-
- const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
- expect(result).toBe(false);
- });
- });
-
- describe('getInputSelectionPosition', () => {
- describe('word with trailing spaces', () => {
- const value = 'label:none ';
-
- it('should return selectionStart when cursor is at the trailing space', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 11,
- value,
- });
-
- expect(left).toBe(11);
- expect(right).toBe(11);
- });
-
- it('should return input when cursor is at the start of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
-
- it('should return input when cursor is at the middle of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 7,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
-
- it('should return input when cursor is at the end of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 10,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
- });
-
- describe('multiple words', () => {
- const value = 'label:~"Community Contribution"';
-
- it('should return input when cursor is after the first word', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 17,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(31);
- });
-
- it('should return input when cursor is before the second word', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 18,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(31);
- });
- });
-
- describe('incomplete multiple words', () => {
- const value = 'label:~"Community Contribution';
-
- it('should return entire input when cursor is at the start of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(30);
- });
-
- it('should return entire input when cursor is at the end of input', () => {
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
- selectionStart: 30,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(30);
- });
- });
- });
- });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
new file mode 100644
index 00000000000..a1da3396d7b
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -0,0 +1,101 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_visual_tokens');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+
+(() => {
+ describe('Filtered Search Dropdown Manager', () => {
+ describe('addWordToInput', () => {
+ function getInputValue() {
+ return document.querySelector('.filtered-search').value;
+ }
+
+ function setInputValue(value) {
+ document.querySelector('.filtered-search').value = value;
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search">
+ </li>
+ </ul>
+ `);
+ });
+
+ describe('input has no existing value', () => {
+ it('should add just tokenName', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('milestone');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('milestone');
+ expect(getInputValue()).toBe('');
+ });
+
+ it('should add tokenName and tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+ let token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(getInputValue()).toBe('');
+
+ gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
+ // We have to get that reference again
+ // Because gl.FilteredSearchDropdownManager deletes the previous token
+ token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('none');
+ expect(getInputValue()).toBe('');
+ });
+ });
+
+ describe('input has existing value', () => {
+ it('should be able to just add tokenName', () => {
+ setInputValue('a');
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(getInputValue()).toBe('');
+ });
+
+ it('should replace tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
+
+ setInputValue('roo');
+ gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(token.querySelector('.value').innerText).toBe('@root');
+ expect(getInputValue()).toBe('');
+ });
+
+ it('should add tokenValues containing spaces', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+ setInputValue('"test ');
+ gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
+ expect(getInputValue()).toBe('');
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
deleted file mode 100644
index ed0b0196ec4..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
+++ /dev/null
@@ -1,59 +0,0 @@
-require('~/extensions/array');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown_manager');
-
-(() => {
- describe('Filtered Search Dropdown Manager', () => {
- describe('addWordToInput', () => {
- function getInputValue() {
- return document.querySelector('.filtered-search').value;
- }
-
- function setInputValue(value) {
- document.querySelector('.filtered-search').value = value;
- }
-
- beforeEach(() => {
- const input = document.createElement('input');
- input.classList.add('filtered-search');
- document.body.appendChild(input);
- });
-
- afterEach(() => {
- document.querySelector('.filtered-search').outerHTML = '';
- });
-
- describe('input has no existing value', () => {
- it('should add just tokenName', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('milestone');
- expect(getInputValue()).toBe('milestone:');
- });
-
- it('should add tokenName and tokenValue', () => {
- gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
- expect(getInputValue()).toBe('label:none ');
- });
- });
-
- describe('input has existing value', () => {
- it('should be able to just add tokenName', () => {
- setInputValue('a');
- gl.FilteredSearchDropdownManager.addWordToInput('author');
- expect(getInputValue()).toBe('author:');
- });
-
- it('should replace tokenValue', () => {
- setInputValue('author:roo');
- gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
- expect(getInputValue()).toBe('author:@root ');
- });
-
- it('should add tokenValues containing spaces', () => {
- setInputValue('label:~"test');
- gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
- expect(getInputValue()).toBe('label:~\'"test me"\' ');
- });
- });
- });
- });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
new file mode 100644
index 00000000000..113161c21c6
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -0,0 +1,262 @@
+require('~/lib/utils/url_utility');
+require('~/lib/utils/common_utils');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+require('~/filtered_search/filtered_search_manager');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
+
+(() => {
+ describe('Filtered Search Manager', () => {
+ let input;
+ let manager;
+ let tokensContainer;
+ const placeholder = 'Search or filter results...';
+
+ function dispatchBackspaceEvent(element, eventType) {
+ const backspaceKey = 8;
+ const event = new Event(eventType);
+ event.keyCode = backspaceKey;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchDeleteEvent(element, eventType) {
+ const deleteKey = 46;
+ const event = new Event(eventType);
+ event.keyCode = deleteKey;
+ element.dispatchEvent(event);
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="filtered-search-input-container">
+ <form>
+ <ul class="tokens-container list-unstyled">
+ ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+ </ul>
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ </div>
+ `);
+
+ spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
+ spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+ spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
+
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new gl.FilteredSearchManager();
+ });
+
+ afterEach(() => {
+ manager.cleanup();
+ });
+
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=✓&state=opened';
+
+ it('should search with a single word', (done) => {
+ input.value = 'searchTerm';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with multiple words', (done) => {
+ input.value = 'awesome search terms';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with special characters', (done) => {
+ input.value = '~!@#$%^&*()_+{}:<>,.?/';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+ done();
+ });
+
+ manager.search();
+ });
+ });
+
+ describe('handleInputPlaceholder', () => {
+ it('should render placeholder when there is no input', () => {
+ expect(input.placeholder).toEqual(placeholder);
+ });
+
+ it('should not render placeholder when there is input', () => {
+ input.value = 'test words';
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+ });
+
+ describe('checkForBackspace', () => {
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+ });
+
+ it('removes last token', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ dispatchBackspaceEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('sets the input', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+ expect(input.value).toEqual('~bug');
+ });
+ });
+
+ it('does not remove token or change input when there is existing input', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+ });
+
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchDeleteEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
+
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
+
+ const clearButton = document.querySelector('.clear-search');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(false);
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(true);
+ expect(getVisualTokens().length).toEqual(0);
+ });
+ });
+
+ describe('unselects token', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
+
+ it('unselects token when input is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+ // Click directly on input attached to document
+ // so that the click event will propagate properly
+ document.querySelector('.filtered-search').click();
+
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ });
+
+ it('unselects token when document.body is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+ document.body.click();
+
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+ });
+ });
+
+ describe('toggleInputContainerFocus', () => {
+ it('toggles on focus', () => {
+ input.focus();
+ expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true);
+ });
+
+ it('toggles on blur', () => {
+ input.blur();
+ expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
deleted file mode 100644
index 98959dda242..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ /dev/null
@@ -1,67 +0,0 @@
-require('~/lib/utils/url_utility');
-require('~/lib/utils/common_utils');
-require('~/filtered_search/filtered_search_token_keys');
-require('~/filtered_search/filtered_search_tokenizer');
-require('~/filtered_search/filtered_search_dropdown_manager');
-require('~/filtered_search/filtered_search_manager');
-
-(() => {
- describe('Filtered Search Manager', () => {
- describe('search', () => {
- let manager;
- const defaultParams = '?scope=all&utf8=✓&state=opened';
-
- function getInput() {
- return document.querySelector('.filtered-search');
- }
-
- beforeEach(() => {
- setFixtures(`
- <input type='text' class='filtered-search' />
- `);
-
- spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
-
- manager = new gl.FilteredSearchManager();
- });
-
- afterEach(() => {
- getInput().outerHTML = '';
- });
-
- it('should search with a single word', () => {
- getInput().value = 'searchTerm';
-
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- });
-
- manager.search();
- });
-
- it('should search with multiple words', () => {
- getInput().value = 'awesome search terms';
-
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
- });
-
- manager.search();
- });
-
- it('should search with special characters', () => {
- getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
-
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
- expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
- });
-
- manager.search();
- });
- });
- });
-})();
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index cf409a7e509..cf409a7e509 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
index a91801cfc89..a91801cfc89 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
new file mode 100644
index 00000000000..bbda1476fed
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -0,0 +1,600 @@
+require('~/filtered_search/filtered_search_visual_tokens');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
+
+describe('Filtered Search Visual Tokens', () => {
+ let tokensContainer;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ </ul>
+ `);
+ tokensContainer = document.querySelector('.tokens-container');
+ });
+
+ describe('getLastVisualTokenBeforeInput', () => {
+ it('returns when there are no visual tokens', () => {
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(null);
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ describe('input is the last item in tokensContainer', () => {
+ it('returns when there is one visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns when there is an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'),
+ );
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+
+ it('returns when there are multiple visual tokens', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns when there are multiple visual tokens and an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+ });
+
+ describe('input is a middle item in tokensContainer', () => {
+ it('returns last token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns last partial token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+ });
+ });
+
+ describe('unselectTokens', () => {
+ it('does nothing when there are no tokens', () => {
+ const beforeHTML = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(tokensContainer.innerHTML).toEqual(beforeHTML);
+ });
+
+ it('removes the selected class from buttons', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
+ `);
+
+ const selected = tokensContainer.querySelector('.js-visual-token .selected');
+ expect(selected.classList.contains('selected')).toEqual(true);
+
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(selected.classList.contains('selected')).toEqual(false);
+ });
+ });
+
+ describe('selectToken', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
+
+ it('removes the selected class if it has selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+ firstTokenButton.classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(false);
+ });
+
+ describe('has no selected class', () => {
+ it('adds selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(true);
+ });
+
+ it('removes selected class from other tokens', () => {
+ const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
+ tokenButtons[1].classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+
+ expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
+ expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
+ });
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ it('does not remove when there are no selected tokens', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+ });
+
+ it('removes selected token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
+ });
+ });
+
+ describe('createVisualTokenElementHTML', () => {
+ let tokenElement;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="test-area">
+ ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+ </div>
+ `);
+
+ tokenElement = document.querySelector('.test-area').firstElementChild;
+ });
+
+ it('contains name div', () => {
+ expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
+ });
+
+ it('contains value div', () => {
+ expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+ });
+
+ it('contains selectable class', () => {
+ expect(tokenElement.classList.contains('selectable')).toEqual(true);
+ });
+
+ it('contains button role', () => {
+ expect(tokenElement.getAttribute('role')).toEqual('button');
+ });
+ });
+
+ describe('addVisualTokenElement', () => {
+ it('renders search visual tokens', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('search term');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('renders filter visual token name', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('renders filter visual token name and value', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('label');
+ expect(token.querySelector('.value').innerText).toEqual('Frontend');
+ });
+
+ it('inserts visual token before input', () => {
+ tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
+
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ const tokens = tokensContainer.querySelectorAll('.js-visual-token');
+ const labelToken = tokens[0];
+ const assigneeToken = tokens[1];
+
+ expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(labelToken.querySelector('.name').innerText).toEqual('label');
+ expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+
+ expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
+ });
+ });
+
+ describe('addValueToPreviousVisualTokenElement', () => {
+ it('does not add when previous visual token element has no value', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('does not add when previous visual token element is a search', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('adds value to previous visual filter token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ const updatedToken = tokensContainer.querySelector('.js-visual-token');
+
+ expect(updatedToken.querySelector('.name').innerText).toEqual('label');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('value');
+ expect(original).not.toEqual(tokensContainer.innerHTML);
+ });
+ });
+
+ describe('addFilterVisualToken', () => {
+ it('creates visual token with just tokenName', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('creates visual token with just tokenValue', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value').innerText).toEqual('%8.17');
+ });
+
+ it('creates full visual token', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('assignee');
+ expect(token.querySelector('.value').innerText).toEqual('@john');
+ });
+ });
+
+ describe('addSearchVisualToken', () => {
+ it('creates search visual token', () => {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('search term');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('appends to previous search visual token if previous token was a search token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+ const token = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(token.querySelector('.name').innerText).toEqual('search term append this');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+ });
+
+ describe('getLastTokenPartial', () => {
+ it('should get last token value', () => {
+ const value = '~bug';
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+ );
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
+ });
+
+ it('should get last token name if there is no value', () => {
+ const name = 'assignee';
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
+ );
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
+ });
+
+ it('should return empty when there are no tokens', () => {
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
+ });
+ });
+
+ describe('removeLastTokenPartial', () => {
+ it('should remove the last token value if it exists', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
+ });
+
+ it('should remove the last token name if there is no value', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
+ });
+
+ it('should not remove anything when there are no tokens', () => {
+ const html = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.innerHTML).toEqual(html);
+ });
+ });
+
+ describe('tokenizeInput', () => {
+ it('does not do anything if there is no input', () => {
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ expect(tokensContainer.innerHTML).toEqual(original);
+ });
+
+ it('adds search visual token if previous visual token is valid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = 'some value';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const newToken = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(input.value).toEqual('');
+ expect(newToken.querySelector('.name').innerText).toEqual('some value');
+ expect(newToken.querySelector('.value')).toEqual(null);
+ });
+
+ it('adds value to previous visual token element if previous visual token is invalid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = '@john';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const updatedToken = tokensContainer.querySelector('.filtered-search-token');
+
+ expect(input.value).toEqual('');
+ expect(updatedToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('@john');
+ });
+ });
+
+ describe('editToken', () => {
+ let input;
+ let token;
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ `);
+
+ input = document.querySelector('.filtered-search');
+ token = document.querySelector('.js-visual-token');
+ });
+
+ it('tokenize\'s existing input', () => {
+ input.value = 'some text';
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(input.value).not.toEqual('some text');
+ });
+
+ it('moves input to the token position', () => {
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
+ });
+
+ it('input contains the visual token value', () => {
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('none');
+ });
+
+ describe('selected token is a search term token', () => {
+ beforeEach(() => {
+ token = document.querySelector('.filtered-search-term');
+ });
+
+ it('token is removed', () => {
+ expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
+ });
+
+ it('input has the same value as removed token', () => {
+ expect(input.value).toEqual('');
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('search');
+ });
+ });
+ });
+
+ describe('moveInputTotheRight', () => {
+ it('does nothing if the input is already the right most element', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
+ );
+
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {});
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+ });
+
+ it('tokenize\'s input', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'none';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const value = tokensContainer.querySelector('.js-visual-token .value');
+
+ expect(value.innerText).toEqual('none');
+ });
+
+ it('converts input into search term token if last token is valid', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'test';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
+
+ expect(searchValue.innerText).toEqual('test');
+ });
+
+ it('moves the input to the right most element', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
+ });
+
+ it('tokenizes input even if input is the right most element', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
+ `;
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ const token = tokensContainer.children[1];
+ expect(token.querySelector('.value').innerText).toEqual('~bug');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
new file mode 100644
index 00000000000..09d8c9df3b2
--- /dev/null
+++ b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
@@ -0,0 +1,2 @@
+%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}}
+ %i.fa.fa-trash-o
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index 0e7c2351b66..a059818145b 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -20,7 +20,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
it 'branches/new_branch.html.raw' do |example|
get :new,
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb
index 978e25a1c32..320de791b08 100644
--- a/spec/javascripts/fixtures/builds.rb
+++ b/spec/javascripts/fixtures/builds.rb
@@ -24,7 +24,7 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller
it 'builds/build-with-artifacts.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: build_with_artifacts.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
deleted file mode 100644
index a50812d9517..00000000000
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable space-before-function-paren */
-(function() {
- window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
-}).call(window);
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
new file mode 100644
index 00000000000..483063fb889
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/metrics.html.haml
@@ -0,0 +1,12 @@
+%div
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Metrics for environment
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' } \ No newline at end of file
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 06f708f9e15..88e3f860809 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -41,7 +41,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
def render_issue(fixture_file_name, issue)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: issue.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 62984097099..ee893b76c84 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -27,7 +27,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
def render_merge_request(fixture_file_name, merge_request)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json
new file mode 100644
index 00000000000..a96a4c0c095
--- /dev/null
+++ b/spec/javascripts/fixtures/project_branches.json
@@ -0,0 +1,5 @@
+[
+ "master",
+ "development",
+ "staging"
+]
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 4ce7f5c601a..1339ee00870 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -43,7 +43,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 0,
"permissions": {
"project_access": null,
@@ -88,7 +88,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
@@ -139,7 +139,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": true,
+ "only_allow_merge_if_pipeline_succeeds": true,
"open_issues_count": 4,
"permissions": {
"project_access": null,
@@ -187,7 +187,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": true,
+ "only_allow_merge_if_pipeline_succeeds": true,
"open_issues_count": 4,
"permissions": {
"project_access": null,
@@ -235,7 +235,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": null,
@@ -283,7 +283,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
@@ -334,7 +334,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 3,
"permissions": {
"project_access": null,
@@ -382,7 +382,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
@@ -433,7 +433,7 @@
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
- "only_allow_merge_if_build_succeeds": false,
+ "only_allow_merge_if_pipeline_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": null,
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 56513219e1e..6c33b240e5c 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -20,7 +20,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
it 'projects/dashboard.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
- id: project.to_param
+ id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
new file mode 100644
index 00000000000..821fb7940a0
--- /dev/null
+++ b/spec/javascripts/fixtures/target_branch_dropdown.html.haml
@@ -0,0 +1,28 @@
+%form.js-edit-blob-form
+ %input{type: 'hidden', name: 'target_branch', value: 'master'}
+ %div
+ .dropdown
+ %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}}
+ .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging
+ .dropdown-page-one
+ .dropdown-title 'Select branch'
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', value: ''}
+ %i.fa.fa-search.dropdown-input-search
+ %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'}
+ .dropdown-content
+ .dropdown-footer
+ %ul.dropdown-footer-list
+ %li
+ %a.create-new-branch.dropdown-toggle-page{href: "#"}
+ Create new branch
+ .dropdown-page-two.dropdown-new-branch
+ %button.dropdown-title-button.dropdown-menu-back{type: 'button'}
+ .dropdown_title 'Create new branch'
+ .dropdown_content
+ %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" }
+ %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" }
+ Create
+ %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" }
+ Cancel
+ %button{type: 'submit'}
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
index 2c08b06ea9e..a81ef8c5492 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -39,8 +39,8 @@ describe 'Todos (JavaScript fixtures)' do
it 'todos/todos.json' do |example|
post :create,
- namespace_id: namespace.path,
- project_id: project.path,
+ namespace_id: namespace,
+ project_id: project,
issuable_type: 'issue',
issuable_id: issue_2.id,
format: 'json'
diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml
new file mode 100644
index 00000000000..275359bde0a
--- /dev/null
+++ b/spec/javascripts/fixtures/user_callout.html.haml
@@ -0,0 +1,2 @@
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js
index 5dfa4008fbd..5dfa4008fbd 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js.es6
+++ b/spec/javascripts/gfm_auto_complete_spec.js
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js
index c207fb00a47..c207fb00a47 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
new file mode 100644
index 00000000000..b2b46640e5b
--- /dev/null
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -0,0 +1,399 @@
+import { glEmojiTag } from '~/behaviors/gl_emoji';
+import {
+ isEmojiUnicodeSupported,
+ isFlagEmoji,
+ isKeycapEmoji,
+ isSkinToneComboEmoji,
+ isHorceRacingSkinToneComboEmoji,
+ isPersonZwjEmoji,
+} from '~/behaviors/gl_emoji/is_emoji_unicode_supported';
+
+const emptySupportMap = {
+ personZwj: false,
+ horseRacing: false,
+ flag: false,
+ skinToneModifier: false,
+ '9.0': false,
+ '8.0': false,
+ '7.0': false,
+ 6.1: false,
+ '6.0': false,
+ 5.2: false,
+ 5.1: false,
+ 4.1: false,
+ '4.0': false,
+ 3.2: false,
+ '3.0': false,
+ 1.1: false,
+};
+
+const emojiFixtureMap = {
+ bomb: {
+ name: 'bomb',
+ moji: '💣',
+ unicodeVersion: '6.0',
+ },
+ construction_worker_tone5: {
+ name: 'construction_worker_tone5',
+ moji: '👷🏿',
+ unicodeVersion: '8.0',
+ },
+ five: {
+ name: 'five',
+ moji: '5️⃣',
+ unicodeVersion: '3.0',
+ },
+ grey_question: {
+ name: 'grey_question',
+ moji: '❔',
+ unicodeVersion: '6.0',
+ },
+};
+
+function markupToDomElement(markup) {
+ const div = document.createElement('div');
+ div.innerHTML = markup;
+ return div.firstElementChild;
+}
+
+function testGlEmojiImageFallback(element, name, src) {
+ expect(element.tagName.toLowerCase()).toBe('img');
+ expect(element.getAttribute('src')).toBe(src);
+ expect(element.getAttribute('title')).toBe(`:${name}:`);
+ expect(element.getAttribute('alt')).toBe(`:${name}:`);
+}
+
+const defaults = {
+ forceFallback: false,
+ sprite: false,
+};
+
+function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
+ const opts = Object.assign({}, defaults, options);
+ expect(element.tagName.toLowerCase()).toBe('gl-emoji');
+ expect(element.dataset.name).toBe(name);
+ expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
+ expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
+
+ const fallbackSpriteClass = `emoji-${name}`;
+ if (opts.sprite) {
+ expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
+ }
+
+ if (opts.forceFallback && opts.sprite) {
+ expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
+ }
+
+ if (opts.forceFallback && !opts.sprite) {
+ // Check for image fallback
+ testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
+ } else {
+ // Otherwise make sure things are still unicode text
+ expect(element.textContent.trim()).toBe(unicodeMoji);
+ }
+}
+
+describe('gl_emoji', () => {
+ describe('glEmojiTag', () => {
+ it('bomb emoji', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('bomb emoji with image fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
+
+ it('bomb emoji with sprite fallback readiness', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ sprite: true,
+ },
+ );
+ });
+ it('bomb emoji with sprite fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ sprite: true,
+ },
+ );
+ });
+
+ it('question mark when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('question mark with image fallback when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
+ });
+
+ describe('isFlagEmoji', () => {
+ it('should detect flag_ac', () => {
+ expect(isFlagEmoji('🇦🇨')).toBeTruthy();
+ });
+ it('should detect flag_us', () => {
+ expect(isFlagEmoji('🇺🇸')).toBeTruthy();
+ });
+ it('should detect flag_zw', () => {
+ expect(isFlagEmoji('🇿🇼')).toBeTruthy();
+ });
+ it('should not detect flags', () => {
+ expect(isFlagEmoji('🎏')).toBeFalsy();
+ });
+ it('should not detect triangular_flag_on_post', () => {
+ expect(isFlagEmoji('🚩')).toBeFalsy();
+ });
+ it('should not detect single letter', () => {
+ expect(isFlagEmoji('🇦')).toBeFalsy();
+ });
+ it('should not detect >2 letters', () => {
+ expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
+ });
+ });
+
+ describe('isKeycapEmoji', () => {
+ it('should detect one(keycap)', () => {
+ expect(isKeycapEmoji('1️⃣')).toBeTruthy();
+ });
+ it('should detect nine(keycap)', () => {
+ expect(isKeycapEmoji('9️⃣')).toBeTruthy();
+ });
+ it('should not detect ten(keycap)', () => {
+ expect(isKeycapEmoji('🔟')).toBeFalsy();
+ });
+ it('should not detect hash(keycap)', () => {
+ expect(isKeycapEmoji('#⃣')).toBeFalsy();
+ });
+ });
+
+ describe('isSkinToneComboEmoji', () => {
+ it('should detect hand_splayed_tone5', () => {
+ expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy();
+ });
+ it('should not detect hand_splayed', () => {
+ expect(isSkinToneComboEmoji('🖐')).toBeFalsy();
+ });
+ it('should detect lifter_tone1', () => {
+ expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy();
+ });
+ it('should not detect lifter', () => {
+ expect(isSkinToneComboEmoji('🏋')).toBeFalsy();
+ });
+ it('should detect rowboat_tone4', () => {
+ expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy();
+ });
+ it('should not detect rowboat', () => {
+ expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
+ });
+ it('should not detect individual tone emoji', () => {
+ expect(isSkinToneComboEmoji('🏻')).toBeFalsy();
+ });
+ });
+
+ describe('isHorceRacingSkinToneComboEmoji', () => {
+ it('should detect horse_racing_tone2', () => {
+ expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy();
+ });
+ it('should not detect horse_racing', () => {
+ expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy();
+ });
+ });
+
+ describe('isPersonZwjEmoji', () => {
+ it('should detect couple_mm', () => {
+ expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy();
+ });
+ it('should not detect couple_with_heart', () => {
+ expect(isPersonZwjEmoji('💑')).toBeFalsy();
+ });
+ it('should not detect couplekiss', () => {
+ expect(isPersonZwjEmoji('💏')).toBeFalsy();
+ });
+ it('should detect family_mmb', () => {
+ expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy();
+ });
+ it('should detect family_mwgb', () => {
+ expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy();
+ });
+ it('should not detect family', () => {
+ expect(isPersonZwjEmoji('👪')).toBeFalsy();
+ });
+ it('should detect kiss_ww', () => {
+ expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy();
+ });
+ it('should not detect girl', () => {
+ expect(isPersonZwjEmoji('👧')).toBeFalsy();
+ });
+ it('should not detect girl_tone5', () => {
+ expect(isPersonZwjEmoji('👧🏿')).toBeFalsy();
+ });
+ it('should not detect man', () => {
+ expect(isPersonZwjEmoji('👨')).toBeFalsy();
+ });
+ it('should not detect woman', () => {
+ expect(isPersonZwjEmoji('👩')).toBeFalsy();
+ });
+ });
+
+ describe('isEmojiUnicodeSupported', () => {
+ it('bomb(6.0) with 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '6.0': true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('bomb(6.0) without 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = emptySupportMap;
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('bomb(6.0) without 6.0 but with 9.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '9.0': true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
+ const emojiKey = 'construction_worker_tone5';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ skinToneModifier: false,
+ '9.0': true,
+ '8.0': true,
+ '7.0': true,
+ 6.1: true,
+ '6.0': true,
+ 5.2: true,
+ 5.1: true,
+ 4.1: true,
+ '4.0': true,
+ 3.2: true,
+ '3.0': true,
+ 1.1: true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('use native keycap on >=57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 57,
+ },
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('fallback keycap on <57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 50,
+ },
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js
index 733023481f5..733023481f5 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js
diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js
index 71d6e2a7e22..71d6e2a7e22 100644
--- a/spec/javascripts/gl_form_spec.js.es6
+++ b/spec/javascripts/gl_form_spec.js
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index a954bb60560..861f26e162f 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,9 +1,7 @@
-/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */
-/* global d3 */
-/* global ContributorsGraph */
-/* global ContributorsMasterGraph */
+/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */
-require('~/graphs/stat_graph_contributors_graph');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph';
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index b15764abe8c..9b47ab62181 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */
-/* global ContributorsStatGraphUtil */
-require('~/graphs/stat_graph_contributors_util');
+import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util';
describe("ContributorsStatGraphUtil", function () {
describe("#parse_log", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
deleted file mode 100644
index 876c23361bc..00000000000
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* eslint-disable quotes */
-/* global StatGraph */
-
-require('~/graphs/stat_graph');
-
-describe("StatGraph", function () {
- describe("#get_log", function () {
- it("returns log", function () {
- StatGraph.log = "test";
- expect(StatGraph.get_log()).toBe("test");
- });
- });
-
- describe("#set_log", function () {
- it("sets the log", function () {
- StatGraph.set_log("test");
- expect(StatGraph.log).toBe("test");
- });
- });
-});
diff --git a/spec/javascripts/helpers/class_spec_helper.js b/spec/javascripts/helpers/class_spec_helper.js
new file mode 100644
index 00000000000..61db27a8fcc
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper.js
@@ -0,0 +1,11 @@
+class ClassSpecHelper {
+ static itShouldBeAStaticMethod(base, method) {
+ return it('should be a static method', () => {
+ expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
+ });
+ }
+}
+
+window.ClassSpecHelper = ClassSpecHelper;
+
+module.exports = ClassSpecHelper;
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6
deleted file mode 100644
index d3c37d39431..00000000000
--- a/spec/javascripts/helpers/class_spec_helper.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-class ClassSpecHelper {
- static itShouldBeAStaticMethod(base, method) {
- return it('should be a static method', () => {
- expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
- });
- }
-}
-
-window.ClassSpecHelper = ClassSpecHelper;
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js
index 0a61e561640..0a61e561640 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js.es6
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
new file mode 100644
index 00000000000..ce83a256ddd
--- /dev/null
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -0,0 +1,52 @@
+class FilteredSearchSpecHelper {
+ static createFilterVisualTokenHTML(name, value, isSelected) {
+ return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML;
+ }
+
+ static createFilterVisualToken(name, value, isSelected = false) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-token');
+
+ li.innerHTML = `
+ <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+ <div class="name">${name}</div>
+ <div class="value">${value}</div>
+ </div>
+ `;
+
+ return li;
+ }
+
+ static createNameFilterVisualTokenHTML(name) {
+ return `
+ <li class="js-visual-token filtered-search-token">
+ <div class="name">${name}</div>
+ </li>
+ `;
+ }
+
+ static createSearchVisualTokenHTML(name) {
+ return `
+ <li class="js-visual-token filtered-search-term">
+ <div class="name">${name}</div>
+ </li>
+ `;
+ }
+
+ static createInputHTML(placeholder = '', value = '') {
+ return `
+ <li class="input-token">
+ <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/>
+ </li>
+ `;
+ }
+
+ static createTokensContainerHTML(html, inputPlaceholder) {
+ return `
+ ${html}
+ ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
+ `;
+ }
+}
+
+module.exports = FilteredSearchSpecHelper;
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js
index 26d87cc5931..26d87cc5931 100644
--- a/spec/javascripts/issuable_spec.js.es6
+++ b/spec/javascripts/issuable_spec.js
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js
index cb068a4f879..cb068a4f879 100644
--- a/spec/javascripts/issuable_time_tracker_spec.js.es6
+++ b/spec/javascripts/issuable_time_tracker_spec.js
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index e7530f61385..aabc8bea12f 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,10 +1,9 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
-/* global Issue */
+import Issue from '~/issue';
require('~/lib/utils/text_utility');
-require('~/issue');
-(function() {
+describe('Issue', function() {
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen;
@@ -59,28 +58,26 @@ require('~/issue');
expect($btnReopen).toHaveText('Reopen issue');
}
- describe('Issue', function() {
- describe('task lists', function() {
- beforeEach(function() {
- loadFixtures('issues/issue-with-task-list.html.raw');
- this.issue = new Issue();
- });
-
- it('modifies the Markdown field', function() {
- spyOn(jQuery, 'ajax').and.stub();
- $('input[type=checkbox]').attr('checked', true).trigger('change');
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
- });
+ describe('task lists', function() {
+ beforeEach(function() {
+ loadFixtures('issues/issue-with-task-list.html.raw');
+ this.issue = new Issue();
+ });
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
- expect(req.data.issue.description).not.toBe(null);
- });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
- $('.js-task-list-field').trigger('tasklist:changed');
+ it('submits an ajax request on tasklist:changed', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PATCH');
+ expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
+ expect(req.data.issue.description).not.toBe(null);
});
+
+ $('.js-task-list-field').trigger('tasklist:changed');
});
});
@@ -139,6 +136,21 @@ require('~/issue');
expectErrorMessage();
expect($('.issue_counter')).toHaveText(1);
});
+
+ it('updates counter', () => {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expectPendingRequest(req, $btnClose);
+ req.success({
+ id: 34
+ });
+ });
+
+ expect($('.issue_counter')).toHaveText(1);
+ $('.issue_counter').text('1,001');
+ expect($('.issue_counter').text()).toEqual('1,001');
+ $btnClose.trigger('click');
+ expect($('.issue_counter').text()).toEqual('1,000');
+ });
});
describe('reopen issue', function() {
@@ -165,4 +177,4 @@ require('~/issue');
expect($('.issue_counter')).toHaveText(1);
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js
index 37e038c16da..37e038c16da 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js
index f4d3e77e515..f4d3e77e515 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
new file mode 100644
index 00000000000..4200e943121
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -0,0 +1,110 @@
+require('~/lib/utils/text_utility');
+
+(() => {
+ describe('text_utility', () => {
+ describe('gl.text.getTextWidth', () => {
+ it('returns zero width when no text is passed', () => {
+ expect(gl.text.getTextWidth('')).toBe(0);
+ });
+
+ it('returns zero width when no text is passed and font is passed', () => {
+ expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+ });
+
+ it('returns width when text is passed', () => {
+ expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+ });
+
+ it('returns bigger width when font is larger', () => {
+ const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+ const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+ expect(largeFont > regular).toBe(true);
+ });
+ });
+
+ describe('gl.text.pluralize', () => {
+ it('returns pluralized', () => {
+ expect(gl.text.pluralize('test', 2)).toBe('tests');
+ });
+
+ it('returns pluralized when count is 0', () => {
+ expect(gl.text.pluralize('test', 0)).toBe('tests');
+ });
+
+ it('does not return pluralized', () => {
+ expect(gl.text.pluralize('test', 1)).toBe('test');
+ });
+ });
+
+ describe('gl.text.highCountTrim', () => {
+ it('returns 99+ for count >= 100', () => {
+ expect(gl.text.highCountTrim(105)).toBe('99+');
+ expect(gl.text.highCountTrim(100)).toBe('99+');
+ });
+
+ it('returns exact number for count < 100', () => {
+ expect(gl.text.highCountTrim(45)).toBe(45);
+ });
+ });
+
+ describe('gl.text.insertText', () => {
+ let textArea;
+
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ });
+
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
+
+ describe('without selection', () => {
+ it('inserts the tag on an empty line', () => {
+ const initialValue = '';
+
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
+
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on a new line if the current one is not empty', () => {
+ const initialValue = 'some text';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}\n* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains spaces', () => {
+ const initialValue = ' ';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains tabs', () => {
+ const initialValue = '\t\t\t';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
deleted file mode 100644
index 06b69b8ac17..00000000000
--- a/spec/javascripts/lib/utils/text_utility_spec.js.es6
+++ /dev/null
@@ -1,50 +0,0 @@
-require('~/lib/utils/text_utility');
-
-(() => {
- describe('text_utility', () => {
- describe('gl.text.getTextWidth', () => {
- it('returns zero width when no text is passed', () => {
- expect(gl.text.getTextWidth('')).toBe(0);
- });
-
- it('returns zero width when no text is passed and font is passed', () => {
- expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
- });
-
- it('returns width when text is passed', () => {
- expect(gl.text.getTextWidth('foo') > 0).toBe(true);
- });
-
- it('returns bigger width when font is larger', () => {
- const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
- const regular = gl.text.getTextWidth('foo', '10px sans-serif');
- expect(largeFont > regular).toBe(true);
- });
- });
-
- describe('gl.text.pluralize', () => {
- it('returns pluralized', () => {
- expect(gl.text.pluralize('test', 2)).toBe('tests');
- });
-
- it('returns pluralized when count is 0', () => {
- expect(gl.text.pluralize('test', 0)).toBe('tests');
- });
-
- it('does not return pluralized', () => {
- expect(gl.text.pluralize('test', 1)).toBe('test');
- });
- });
-
- describe('gl.text.highCountTrim', () => {
- it('returns 99+ for count >= 100', () => {
- expect(gl.text.highCountTrim(105)).toBe('99+');
- expect(gl.text.highCountTrim(100)).toBe('99+');
- });
-
- it('returns exact number for count < 100', () => {
- expect(gl.text.highCountTrim(45)).toBe(45);
- });
- });
- });
-})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index a0b2ebc221b..a1fd2d38968 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -7,16 +7,12 @@ require('~/line_highlighter');
describe('LineHighlighter', function() {
var clickLine;
preloadFixtures('static/line_highlighter.html.raw');
- clickLine = function(number, eventData) {
- var e;
- if (eventData == null) {
- eventData = {};
- }
+ clickLine = function(number, eventData = {}) {
if ($.isEmptyObject(eventData)) {
- return $("#L" + number).mousedown().click();
+ return $("#L" + number).click();
} else {
- e = $.Event('mousedown', eventData);
- return $("#L" + number).trigger(e).click();
+ const e = $.Event('click', eventData);
+ return $("#L" + number).trigger(e);
}
};
beforeEach(function() {
@@ -63,12 +59,6 @@ require('~/line_highlighter');
});
});
describe('#clickHandler', function() {
- it('discards the mousedown event', function() {
- var spy;
- spy = spyOnEvent('a[data-line-number]', 'mousedown');
- clickLine(13);
- return expect(spy).toHaveBeenPrevented();
- });
it('handles clicking on a child icon element', function() {
var spy;
spy = spyOn(this["class"], 'setHash').and.callThrough();
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
new file mode 100644
index 00000000000..e504d41d4d4
--- /dev/null
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -0,0 +1,72 @@
+/* eslint-disable no-new */
+
+import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import '~/flash';
+
+(() => {
+ describe('Mini Pipeline Graph Dropdown', () => {
+ preloadFixtures('static/mini_dropdown_graph.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/mini_dropdown_graph.html.raw');
+ });
+
+ describe('When is initialized', () => {
+ it('should initialize without errors when no options are given', () => {
+ const miniPipelineGraph = new MiniPipelineGraph();
+
+ expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+ });
+
+ it('should set the container as the given prop', () => {
+ const container = '.foo';
+
+ const miniPipelineGraph = new MiniPipelineGraph({ container });
+
+ expect(miniPipelineGraph.container).toEqual(container);
+ });
+ });
+
+ describe('When dropdown is clicked', () => {
+ it('should call getBuildsList', () => {
+ const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ expect(getBuildsListSpy).toHaveBeenCalled();
+ });
+
+ it('should make a request to the endpoint provided in the html', () => {
+ const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+ expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ });
+
+ it('should not close when user uses cmd/ctrl + click', () => {
+ spyOn($, 'ajax').and.callFake(function (params) {
+ params.success({
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
+ });
+ });
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
deleted file mode 100644
index 7cdade01e00..00000000000
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
+++ /dev/null
@@ -1,51 +0,0 @@
-/* eslint-disable no-new */
-
-require('~/flash');
-require('~/mini_pipeline_graph_dropdown');
-
-(() => {
- describe('Mini Pipeline Graph Dropdown', () => {
- preloadFixtures('static/mini_dropdown_graph.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/mini_dropdown_graph.html.raw');
- });
-
- describe('When is initialized', () => {
- it('should initialize without errors when no options are given', () => {
- const miniPipelineGraph = new window.gl.MiniPipelineGraph();
-
- expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
- });
-
- it('should set the container as the given prop', () => {
- const container = '.foo';
-
- const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container });
-
- expect(miniPipelineGraph.container).toEqual(container);
- });
- });
-
- describe('When dropdown is clicked', () => {
- it('should call getBuildsList', () => {
- const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
-
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
-
- document.querySelector('.js-builds-dropdown-button').click();
-
- expect(getBuildsListSpy).toHaveBeenCalled();
- });
-
- it('should make a request to the endpoint provided in the html', () => {
- const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
-
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
-
- document.querySelector('.js-builds-dropdown-button').click();
- expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
- });
- });
- });
-})();
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
new file mode 100644
index 00000000000..a3c1c5e1b7c
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -0,0 +1,75 @@
+import 'jquery';
+import '~/lib/utils/common_utils';
+import PrometheusGraph from '~/monitoring/prometheus_graph';
+import { prometheusMockData } from './prometheus_mock_data';
+
+describe('PrometheusGraph', () => {
+ const fixtureName = 'static/environments/metrics.html.raw';
+ const prometheusGraphContainer = '.prometheus-graph';
+ const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
+
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ this.prometheusGraph = new PrometheusGraph();
+ const self = this;
+ const fakeInit = (metricsResponse) => {
+ self.prometheusGraph.transformData(metricsResponse);
+ self.prometheusGraph.createGraph();
+ };
+ spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit);
+ });
+
+ it('initializes graph properties', () => {
+ // Test for the measurements
+ expect(this.prometheusGraph.margin).toBeDefined();
+ expect(this.prometheusGraph.marginLabelContainer).toBeDefined();
+ expect(this.prometheusGraph.originalWidth).toBeDefined();
+ expect(this.prometheusGraph.originalHeight).toBeDefined();
+ expect(this.prometheusGraph.height).toBeDefined();
+ expect(this.prometheusGraph.width).toBeDefined();
+ expect(this.prometheusGraph.backOffRequestCounter).toBeDefined();
+ // Test for the graph properties (colors, radius, etc.)
+ expect(this.prometheusGraph.graphSpecificProperties).toBeDefined();
+ expect(this.prometheusGraph.commonGraphProperties).toBeDefined();
+ });
+
+ it('transforms the data', () => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ expect(this.prometheusGraph.data).toBeDefined();
+ expect(this.prometheusGraph.data.cpu_values.length).toBe(121);
+ expect(this.prometheusGraph.data.memory_values.length).toBe(121);
+ });
+
+ it('creates two graphs', () => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ expect($(prometheusGraphContainer).length).toBe(2);
+ });
+
+ describe('Graph contents', () => {
+ beforeEach(() => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ });
+
+ it('has axis, an area, a line and a overlay', () => {
+ const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent();
+ expect($graphContainer.find('.x-axis')).toBeDefined();
+ expect($graphContainer.find('.y-axis')).toBeDefined();
+ expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined();
+ expect($graphContainer.find('.metric-line')).toBeDefined();
+ expect($graphContainer.find('.metric-area')).toBeDefined();
+ });
+
+ it('has legends, labels and an extra axis that labels the metrics', () => {
+ const $prometheusGraphContents = $(prometheusGraphContents);
+ const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent();
+ expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined();
+ expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
+ expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
+ expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
+ expect($axisLabelContainer.find('rect').length).toBe(2);
+ expect($axisLabelContainer.find('text').length).toBe(4);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js
new file mode 100644
index 00000000000..1cdc14faaa8
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_mock_data.js
@@ -0,0 +1,1014 @@
+/* eslint-disable import/prefer-default-export*/
+export const prometheusMockData = {
+ status: 200,
+ metrics: {
+ success: true,
+ metrics: {
+ memory_values: [
+ {
+ metric: {
+ },
+ values: [
+ [
+ 1488462917.256,
+ '10.12890625',
+ ],
+ [
+ 1488462977.256,
+ '10.140625',
+ ],
+ [
+ 1488463037.256,
+ '10.140625',
+ ],
+ [
+ 1488463097.256,
+ '10.14453125',
+ ],
+ [
+ 1488463157.256,
+ '10.1484375',
+ ],
+ [
+ 1488463217.256,
+ '10.15625',
+ ],
+ [
+ 1488463277.256,
+ '10.15625',
+ ],
+ [
+ 1488463337.256,
+ '10.15625',
+ ],
+ [
+ 1488463397.256,
+ '10.1640625',
+ ],
+ [
+ 1488463457.256,
+ '10.171875',
+ ],
+ [
+ 1488463517.256,
+ '10.171875',
+ ],
+ [
+ 1488463577.256,
+ '10.171875',
+ ],
+ [
+ 1488463637.256,
+ '10.18359375',
+ ],
+ [
+ 1488463697.256,
+ '10.1953125',
+ ],
+ [
+ 1488463757.256,
+ '10.203125',
+ ],
+ [
+ 1488463817.256,
+ '10.20703125',
+ ],
+ [
+ 1488463877.256,
+ '10.20703125',
+ ],
+ [
+ 1488463937.256,
+ '10.20703125',
+ ],
+ [
+ 1488463997.256,
+ '10.20703125',
+ ],
+ [
+ 1488464057.256,
+ '10.2109375',
+ ],
+ [
+ 1488464117.256,
+ '10.2109375',
+ ],
+ [
+ 1488464177.256,
+ '10.2109375',
+ ],
+ [
+ 1488464237.256,
+ '10.2109375',
+ ],
+ [
+ 1488464297.256,
+ '10.21484375',
+ ],
+ [
+ 1488464357.256,
+ '10.22265625',
+ ],
+ [
+ 1488464417.256,
+ '10.22265625',
+ ],
+ [
+ 1488464477.256,
+ '10.2265625',
+ ],
+ [
+ 1488464537.256,
+ '10.23046875',
+ ],
+ [
+ 1488464597.256,
+ '10.23046875',
+ ],
+ [
+ 1488464657.256,
+ '10.234375',
+ ],
+ [
+ 1488464717.256,
+ '10.234375',
+ ],
+ [
+ 1488464777.256,
+ '10.234375',
+ ],
+ [
+ 1488464837.256,
+ '10.234375',
+ ],
+ [
+ 1488464897.256,
+ '10.234375',
+ ],
+ [
+ 1488464957.256,
+ '10.234375',
+ ],
+ [
+ 1488465017.256,
+ '10.23828125',
+ ],
+ [
+ 1488465077.256,
+ '10.23828125',
+ ],
+ [
+ 1488465137.256,
+ '10.2421875',
+ ],
+ [
+ 1488465197.256,
+ '10.2421875',
+ ],
+ [
+ 1488465257.256,
+ '10.2421875',
+ ],
+ [
+ 1488465317.256,
+ '10.2421875',
+ ],
+ [
+ 1488465377.256,
+ '10.2421875',
+ ],
+ [
+ 1488465437.256,
+ '10.2421875',
+ ],
+ [
+ 1488465497.256,
+ '10.2421875',
+ ],
+ [
+ 1488465557.256,
+ '10.2421875',
+ ],
+ [
+ 1488465617.256,
+ '10.2421875',
+ ],
+ [
+ 1488465677.256,
+ '10.2421875',
+ ],
+ [
+ 1488465737.256,
+ '10.2421875',
+ ],
+ [
+ 1488465797.256,
+ '10.24609375',
+ ],
+ [
+ 1488465857.256,
+ '10.25',
+ ],
+ [
+ 1488465917.256,
+ '10.25390625',
+ ],
+ [
+ 1488465977.256,
+ '9.98828125',
+ ],
+ [
+ 1488466037.256,
+ '9.9921875',
+ ],
+ [
+ 1488466097.256,
+ '9.9921875',
+ ],
+ [
+ 1488466157.256,
+ '9.99609375',
+ ],
+ [
+ 1488466217.256,
+ '10',
+ ],
+ [
+ 1488466277.256,
+ '10.00390625',
+ ],
+ [
+ 1488466337.256,
+ '10.0078125',
+ ],
+ [
+ 1488466397.256,
+ '10.01171875',
+ ],
+ [
+ 1488466457.256,
+ '10.0234375',
+ ],
+ [
+ 1488466517.256,
+ '10.02734375',
+ ],
+ [
+ 1488466577.256,
+ '10.02734375',
+ ],
+ [
+ 1488466637.256,
+ '10.03125',
+ ],
+ [
+ 1488466697.256,
+ '10.03125',
+ ],
+ [
+ 1488466757.256,
+ '10.03125',
+ ],
+ [
+ 1488466817.256,
+ '10.03125',
+ ],
+ [
+ 1488466877.256,
+ '10.03125',
+ ],
+ [
+ 1488466937.256,
+ '10.03125',
+ ],
+ [
+ 1488466997.256,
+ '10.03125',
+ ],
+ [
+ 1488467057.256,
+ '10.0390625',
+ ],
+ [
+ 1488467117.256,
+ '10.0390625',
+ ],
+ [
+ 1488467177.256,
+ '10.04296875',
+ ],
+ [
+ 1488467237.256,
+ '10.05078125',
+ ],
+ [
+ 1488467297.256,
+ '10.05859375',
+ ],
+ [
+ 1488467357.256,
+ '10.06640625',
+ ],
+ [
+ 1488467417.256,
+ '10.06640625',
+ ],
+ [
+ 1488467477.256,
+ '10.0703125',
+ ],
+ [
+ 1488467537.256,
+ '10.07421875',
+ ],
+ [
+ 1488467597.256,
+ '10.0859375',
+ ],
+ [
+ 1488467657.256,
+ '10.0859375',
+ ],
+ [
+ 1488467717.256,
+ '10.09765625',
+ ],
+ [
+ 1488467777.256,
+ '10.1015625',
+ ],
+ [
+ 1488467837.256,
+ '10.10546875',
+ ],
+ [
+ 1488467897.256,
+ '10.10546875',
+ ],
+ [
+ 1488467957.256,
+ '10.125',
+ ],
+ [
+ 1488468017.256,
+ '10.13671875',
+ ],
+ [
+ 1488468077.256,
+ '10.1484375',
+ ],
+ [
+ 1488468137.256,
+ '10.15625',
+ ],
+ [
+ 1488468197.256,
+ '10.16796875',
+ ],
+ [
+ 1488468257.256,
+ '10.171875',
+ ],
+ [
+ 1488468317.256,
+ '10.171875',
+ ],
+ [
+ 1488468377.256,
+ '10.171875',
+ ],
+ [
+ 1488468437.256,
+ '10.171875',
+ ],
+ [
+ 1488468497.256,
+ '10.171875',
+ ],
+ [
+ 1488468557.256,
+ '10.171875',
+ ],
+ [
+ 1488468617.256,
+ '10.171875',
+ ],
+ [
+ 1488468677.256,
+ '10.17578125',
+ ],
+ [
+ 1488468737.256,
+ '10.17578125',
+ ],
+ [
+ 1488468797.256,
+ '10.265625',
+ ],
+ [
+ 1488468857.256,
+ '10.19921875',
+ ],
+ [
+ 1488468917.256,
+ '10.19921875',
+ ],
+ [
+ 1488468977.256,
+ '10.19921875',
+ ],
+ [
+ 1488469037.256,
+ '10.19921875',
+ ],
+ [
+ 1488469097.256,
+ '10.19921875',
+ ],
+ [
+ 1488469157.256,
+ '10.203125',
+ ],
+ [
+ 1488469217.256,
+ '10.43359375',
+ ],
+ [
+ 1488469277.256,
+ '10.20703125',
+ ],
+ [
+ 1488469337.256,
+ '10.2109375',
+ ],
+ [
+ 1488469397.256,
+ '10.22265625',
+ ],
+ [
+ 1488469457.256,
+ '10.21484375',
+ ],
+ [
+ 1488469517.256,
+ '10.21484375',
+ ],
+ [
+ 1488469577.256,
+ '10.21484375',
+ ],
+ [
+ 1488469637.256,
+ '10.22265625',
+ ],
+ [
+ 1488469697.256,
+ '10.234375',
+ ],
+ [
+ 1488469757.256,
+ '10.234375',
+ ],
+ [
+ 1488469817.256,
+ '10.234375',
+ ],
+ [
+ 1488469877.256,
+ '10.2421875',
+ ],
+ [
+ 1488469937.256,
+ '10.25',
+ ],
+ [
+ 1488469997.256,
+ '10.25390625',
+ ],
+ [
+ 1488470057.256,
+ '10.26171875',
+ ],
+ [
+ 1488470117.256,
+ '10.2734375',
+ ],
+ ],
+ },
+ ],
+ memory_current: [
+ {
+ metric: {
+ },
+ value: [
+ 1488470117.737,
+ '10.2734375',
+ ],
+ },
+ ],
+ cpu_values: [
+ {
+ metric: {
+ },
+ values: [
+ [
+ 1488462918.15,
+ '0.0002996458625058103',
+ ],
+ [
+ 1488462978.15,
+ '0.0002652382333333314',
+ ],
+ [
+ 1488463038.15,
+ '0.0003485461333333421',
+ ],
+ [
+ 1488463098.15,
+ '0.0003420421999999886',
+ ],
+ [
+ 1488463158.15,
+ '0.00023107150000001297',
+ ],
+ [
+ 1488463218.15,
+ '0.00030463981666664826',
+ ],
+ [
+ 1488463278.15,
+ '0.0002477177833333677',
+ ],
+ [
+ 1488463338.15,
+ '0.00026936656666665115',
+ ],
+ [
+ 1488463398.15,
+ '0.000406264750000022',
+ ],
+ [
+ 1488463458.15,
+ '0.00029592802026561453',
+ ],
+ [
+ 1488463518.15,
+ '0.00023426999683316343',
+ ],
+ [
+ 1488463578.15,
+ '0.0003057080666666915',
+ ],
+ [
+ 1488463638.15,
+ '0.0003408470500000149',
+ ],
+ [
+ 1488463698.15,
+ '0.00025497336666665166',
+ ],
+ [
+ 1488463758.15,
+ '0.0003009282833333534',
+ ],
+ [
+ 1488463818.15,
+ '0.0003119383499999924',
+ ],
+ [
+ 1488463878.15,
+ '0.00028719019999998705',
+ ],
+ [
+ 1488463938.15,
+ '0.000327864749999988',
+ ],
+ [
+ 1488463998.15,
+ '0.0002514917333333422',
+ ],
+ [
+ 1488464058.15,
+ '0.0003614651166666742',
+ ],
+ [
+ 1488464118.15,
+ '0.0003221668000000122',
+ ],
+ [
+ 1488464178.15,
+ '0.00023323083333330884',
+ ],
+ [
+ 1488464238.15,
+ '0.00028531499475009274',
+ ],
+ [
+ 1488464298.15,
+ '0.0002627695294921391',
+ ],
+ [
+ 1488464358.15,
+ '0.00027145463333333453',
+ ],
+ [
+ 1488464418.15,
+ '0.00025669488333335266',
+ ],
+ [
+ 1488464478.15,
+ '0.00022307761666665965',
+ ],
+ [
+ 1488464538.15,
+ '0.0003307265833333517',
+ ],
+ [
+ 1488464598.15,
+ '0.0002817050666666709',
+ ],
+ [
+ 1488464658.15,
+ '0.00022357458333332285',
+ ],
+ [
+ 1488464718.15,
+ '0.00032648590000000275',
+ ],
+ [
+ 1488464778.15,
+ '0.00028410750000000816',
+ ],
+ [
+ 1488464838.15,
+ '0.0003038076999999954',
+ ],
+ [
+ 1488464898.15,
+ '0.00037568226666667335',
+ ],
+ [
+ 1488464958.15,
+ '0.00020160354999999202',
+ ],
+ [
+ 1488465018.15,
+ '0.0003229403333333399',
+ ],
+ [
+ 1488465078.15,
+ '0.00033516069999999236',
+ ],
+ [
+ 1488465138.15,
+ '0.0003365978333333371',
+ ],
+ [
+ 1488465198.15,
+ '0.00020262178333331585',
+ ],
+ [
+ 1488465258.15,
+ '0.00040567498333331876',
+ ],
+ [
+ 1488465318.15,
+ '0.00029114155000001436',
+ ],
+ [
+ 1488465378.15,
+ '0.0002498841000000122',
+ ],
+ [
+ 1488465438.15,
+ '0.00027296763333331715',
+ ],
+ [
+ 1488465498.15,
+ '0.0002958794000000135',
+ ],
+ [
+ 1488465558.15,
+ '0.0002922354666666867',
+ ],
+ [
+ 1488465618.15,
+ '0.00034186624999999653',
+ ],
+ [
+ 1488465678.15,
+ '0.0003397984166666627',
+ ],
+ [
+ 1488465738.15,
+ '0.0002658284166666469',
+ ],
+ [
+ 1488465798.15,
+ '0.00026221139999999346',
+ ],
+ [
+ 1488465858.15,
+ '0.00029467960000001034',
+ ],
+ [
+ 1488465918.15,
+ '0.0002634141333333358',
+ ],
+ [
+ 1488465978.15,
+ '0.0003202958333333209',
+ ],
+ [
+ 1488466038.15,
+ '0.00037890760000000394',
+ ],
+ [
+ 1488466098.15,
+ '0.00023453356666666518',
+ ],
+ [
+ 1488466158.15,
+ '0.0002866827333333433',
+ ],
+ [
+ 1488466218.15,
+ '0.0003335935499999998',
+ ],
+ [
+ 1488466278.15,
+ '0.00022787131666666125',
+ ],
+ [
+ 1488466338.15,
+ '0.00033821938333333064',
+ ],
+ [
+ 1488466398.15,
+ '0.00029233375000001043',
+ ],
+ [
+ 1488466458.15,
+ '0.00026562758333333514',
+ ],
+ [
+ 1488466518.15,
+ '0.0003142600999999819',
+ ],
+ [
+ 1488466578.15,
+ '0.00027392178333333444',
+ ],
+ [
+ 1488466638.15,
+ '0.00028178598333334173',
+ ],
+ [
+ 1488466698.15,
+ '0.0002463400666666911',
+ ],
+ [
+ 1488466758.15,
+ '0.00040234373333332125',
+ ],
+ [
+ 1488466818.15,
+ '0.00023677453333332822',
+ ],
+ [
+ 1488466878.15,
+ '0.00030852703333333523',
+ ],
+ [
+ 1488466938.15,
+ '0.0003582272833333455',
+ ],
+ [
+ 1488466998.15,
+ '0.0002176380833332973',
+ ],
+ [
+ 1488467058.15,
+ '0.00026180203333335447',
+ ],
+ [
+ 1488467118.15,
+ '0.00027862966666667436',
+ ],
+ [
+ 1488467178.15,
+ '0.0002769731166666567',
+ ],
+ [
+ 1488467238.15,
+ '0.0002832899166666477',
+ ],
+ [
+ 1488467298.15,
+ '0.0003446533500000311',
+ ],
+ [
+ 1488467358.15,
+ '0.0002691345999999761',
+ ],
+ [
+ 1488467418.15,
+ '0.000284919933333357',
+ ],
+ [
+ 1488467478.15,
+ '0.0002396026166666528',
+ ],
+ [
+ 1488467538.15,
+ '0.00035625295000002075',
+ ],
+ [
+ 1488467598.15,
+ '0.00036759816666664946',
+ ],
+ [
+ 1488467658.15,
+ '0.00030326608333333855',
+ ],
+ [
+ 1488467718.15,
+ '0.00023584972418043393',
+ ],
+ [
+ 1488467778.15,
+ '0.00025744508892115107',
+ ],
+ [
+ 1488467838.15,
+ '0.00036737541666663395',
+ ],
+ [
+ 1488467898.15,
+ '0.00034325741666666094',
+ ],
+ [
+ 1488467958.15,
+ '0.00026390046666667407',
+ ],
+ [
+ 1488468018.15,
+ '0.0003302534500000102',
+ ],
+ [
+ 1488468078.15,
+ '0.00035243794999999527',
+ ],
+ [
+ 1488468138.15,
+ '0.00020149738333333407',
+ ],
+ [
+ 1488468198.15,
+ '0.0003183469666666679',
+ ],
+ [
+ 1488468258.15,
+ '0.0003835329166666845',
+ ],
+ [
+ 1488468318.15,
+ '0.0002485075333333124',
+ ],
+ [
+ 1488468378.15,
+ '0.0003011457166666768',
+ ],
+ [
+ 1488468438.15,
+ '0.00032242785497684965',
+ ],
+ [
+ 1488468498.15,
+ '0.0002659713747457531',
+ ],
+ [
+ 1488468558.15,
+ '0.0003476860333333202',
+ ],
+ [
+ 1488468618.15,
+ '0.00028336403333334794',
+ ],
+ [
+ 1488468678.15,
+ '0.00017132354999998728',
+ ],
+ [
+ 1488468738.15,
+ '0.0003001915833333276',
+ ],
+ [
+ 1488468798.15,
+ '0.0003025715666666725',
+ ],
+ [
+ 1488468858.15,
+ '0.0003012370166666815',
+ ],
+ [
+ 1488468918.15,
+ '0.00030203619999997025',
+ ],
+ [
+ 1488468978.15,
+ '0.0002804355000000314',
+ ],
+ [
+ 1488469038.15,
+ '0.00033194884999998564',
+ ],
+ [
+ 1488469098.15,
+ '0.00025201496666665455',
+ ],
+ [
+ 1488469158.15,
+ '0.0002777531500000189',
+ ],
+ [
+ 1488469218.15,
+ '0.0003314885833333392',
+ ],
+ [
+ 1488469278.15,
+ '0.0002234891422095589',
+ ],
+ [
+ 1488469338.15,
+ '0.000349117355867791',
+ ],
+ [
+ 1488469398.15,
+ '0.0004036731333333303',
+ ],
+ [
+ 1488469458.15,
+ '0.00024553911666667835',
+ ],
+ [
+ 1488469518.15,
+ '0.0003056456833333184',
+ ],
+ [
+ 1488469578.15,
+ '0.0002618737166666681',
+ ],
+ [
+ 1488469638.15,
+ '0.00022972643333331414',
+ ],
+ [
+ 1488469698.15,
+ '0.0003713522500000307',
+ ],
+ [
+ 1488469758.15,
+ '0.00018322576666666515',
+ ],
+ [
+ 1488469818.15,
+ '0.00034534762753952466',
+ ],
+ [
+ 1488469878.15,
+ '0.00028200510008501677',
+ ],
+ [
+ 1488469938.15,
+ '0.0002773708499999768',
+ ],
+ [
+ 1488469998.15,
+ '0.00027547160000001013',
+ ],
+ [
+ 1488470058.15,
+ '0.00031713610000000023',
+ ],
+ [
+ 1488470118.15,
+ '0.00035276853333332525',
+ ],
+ ],
+ },
+ ],
+ cpu_current: [
+ {
+ metric: {
+ },
+ value: [
+ 1488470118.566,
+ '0.00035276853333332525',
+ ],
+ },
+ ],
+ last_update: '2017-03-02T15:55:18.981Z',
+ },
+ },
+};
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index f132537b943..90a429beeca 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
/* global NewBranchForm */
-require('jquery-ui/ui/autocomplete');
require('~/new_branch_form');
(function() {
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
new file mode 100644
index 00000000000..d966226909b
--- /dev/null
+++ b/spec/javascripts/pager_spec.js
@@ -0,0 +1,90 @@
+/* global fixture */
+
+require('~/pager');
+
+describe('pager', () => {
+ const Pager = window.Pager;
+
+ it('is defined on window', () => {
+ expect(window.Pager).toBeDefined();
+ });
+
+ describe('init', () => {
+ const originalHref = window.location.href;
+
+ beforeEach(() => {
+ setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ spyOn($, 'ajax');
+ });
+
+ afterEach(() => {
+ window.history.replaceState({}, null, originalHref);
+ });
+
+ it('should use data-href attribute from list element', () => {
+ const href = `${gl.TEST_HOST}/some_list.json`;
+ setFixtures(`<div class="content_list" data-href="${href}"></div>`);
+ Pager.init();
+ expect(Pager.url).toBe(href);
+ });
+
+ it('should use current url if data-href attribute not provided', () => {
+ const href = `${gl.TEST_HOST}/some_list`;
+ spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ Pager.init();
+ expect(Pager.url).toBe(href);
+ });
+
+ it('should get initial offset from query parameter', () => {
+ window.history.replaceState({}, null, '?offset=100');
+ Pager.init();
+ expect(Pager.offset).toBe(100);
+ });
+
+ it('keeps extra query parameters from url', () => {
+ window.history.replaceState({}, null, '?filter=test&offset=100');
+ const href = `${gl.TEST_HOST}/some_list?filter=test`;
+ spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ Pager.init();
+ expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(Pager.url).toEqual(href);
+ });
+ });
+
+ describe('getOld', () => {
+ beforeEach(() => {
+ setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ Pager.init();
+ });
+
+ it('shows loader while loading next page', () => {
+ spyOn(Pager.loading, 'show');
+ Pager.getOld();
+ expect(Pager.loading.show).toHaveBeenCalled();
+ });
+
+ it('hides loader on success', () => {
+ spyOn($, 'ajax').and.callFake(options => options.success({}));
+ spyOn(Pager.loading, 'hide');
+ Pager.getOld();
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ });
+
+ it('hides loader on error', () => {
+ spyOn($, 'ajax').and.callFake(options => options.error());
+ spyOn(Pager.loading, 'hide');
+ Pager.getOld();
+ expect(Pager.loading.hide).toHaveBeenCalled();
+ });
+
+ it('sends request to url with offset and limit params', () => {
+ spyOn($, 'ajax');
+ Pager.offset = 100;
+ Pager.limit = 20;
+ Pager.getOld();
+ const [{ data, url }] = $.ajax.calls.argsFor(0);
+ expect(data).toBe('limit=20&offset=100');
+ expect(url).toBe('/some_list');
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js
index 72770a702d3..72770a702d3 100644
--- a/spec/javascripts/pipelines_spec.js.es6
+++ b/spec/javascripts/pipelines_spec.js
diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js
new file mode 100644
index 00000000000..ecaaf1907ea
--- /dev/null
+++ b/spec/javascripts/polyfills/element_spec.js
@@ -0,0 +1,36 @@
+import '~/commons/polyfills/element';
+
+describe('Element polyfills', function () {
+ beforeEach(() => {
+ this.element = document.createElement('ul');
+ });
+
+ describe('matches', () => {
+ it('returns true if element matches the selector', () => {
+ expect(this.element.matches('ul')).toBeTruthy();
+ });
+
+ it("returns false if element doesn't match the selector", () => {
+ expect(this.element.matches('.not-an-element')).toBeFalsy();
+ });
+ });
+
+ describe('closest', () => {
+ beforeEach(() => {
+ this.childElement = document.createElement('li');
+ this.element.appendChild(this.childElement);
+ });
+
+ it('returns the closest parent that matches the selector', () => {
+ expect(this.childElement.closest('ul').toString()).toBe(this.element.toString());
+ });
+
+ it('returns itself if it matches the selector', () => {
+ expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString());
+ });
+
+ it('returns undefined if nothing matches the selector', () => {
+ expect(this.childElement.closest('.no-an-element')).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js
index a4662cfb557..a4662cfb557 100644
--- a/spec/javascripts/pretty_time_spec.js.es6
+++ b/spec/javascripts/pretty_time_spec.js
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 69d9587771f..3a1d4e2440f 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -26,7 +26,7 @@ require('~/project');
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
- expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 });
+ expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true });
d = $.Deferred();
d.resolve(this.projects_data);
return d.promise();
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 4ac7e911740..285b7940174 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
/* global Sidebar */
-require('~/right_sidebar');
-require('~/extensions/jquery.js');
+import '~/commons/bootstrap';
+import '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index ffff643e371..9e19dabd0e3 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -31,13 +31,9 @@ require('~/shortcuts_issuable');
this.shortcut.replyWithSelectedText();
expect($(this.selector).val()).toBe('');
});
- it('triggers `input`', function() {
- var focused = false;
- $(this.selector).on('focus', function() {
- focused = true;
- });
+ it('triggers `focus`', function() {
this.shortcut.replyWithSelectedText();
- expect(focused).toBe(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
});
describe('with any selection', function() {
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js
index d83d9a57b42..d83d9a57b42 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js
index 4366ec2a5b8..4366ec2a5b8 100644
--- a/spec/javascripts/smart_interval_spec.js.es6
+++ b/spec/javascripts/smart_interval_spec.js
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js
index 454386697f5..454386697f5 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index ca707d872a4..5cdb6473eda 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -5,23 +5,12 @@ jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
// include common libraries
+require('~/commons/index.js');
window.$ = window.jQuery = require('jquery');
window._ = require('underscore');
window.Cookies = require('js-cookie');
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-require('jquery-ujs');
-require('bootstrap/js/affix');
-require('bootstrap/js/alert');
-require('bootstrap/js/button');
-require('bootstrap/js/collapse');
-require('bootstrap/js/dropdown');
-require('bootstrap/js/modal');
-require('bootstrap/js/scrollspy');
-require('bootstrap/js/tab');
-require('bootstrap/js/transition');
-require('bootstrap/js/tooltip');
-require('bootstrap/js/popover');
// stub expected globals
window.gl = window.gl || {};
@@ -43,10 +32,11 @@ testsContext.keys().forEach(function (path) {
}
});
-// workaround: include all source files to find files with 0% coverage
-// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
-describe('Uncovered files', function () {
- // the following files throw errors because of undefined variables
+// if we're generating coverage reports, make sure to include all files so
+// that we can catch files with 0% coverage
+// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
+if (process.env.BABEL_ENV === 'coverage') {
+ // exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_edit_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
@@ -59,21 +49,23 @@ describe('Uncovered files', function () {
'./network/branch_graph.js',
];
- const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/);
- sourceFiles.keys().forEach(function (path) {
- // ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) {
- return;
- }
+ describe('Uncovered files', function () {
+ const sourceFiles = require.context('~', true, /\.js$/);
+ sourceFiles.keys().forEach(function (path) {
+ // ignore if there is a matching spec file
+ if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+ return;
+ }
- it(`includes '${path}'`, function () {
- try {
- sourceFiles(path);
- } catch (err) {
- if (troubleMakers.indexOf(path) === -1) {
- expect(err).toBeNull();
+ it(`includes '${path}'`, function () {
+ try {
+ sourceFiles(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
}
- }
+ });
});
});
-});
+}
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
new file mode 100644
index 00000000000..205e72af600
--- /dev/null
+++ b/spec/javascripts/user_callout_spec.js
@@ -0,0 +1,57 @@
+const UserCallout = require('~/user_callout');
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+const Cookie = window.Cookies;
+
+describe('UserCallout', function () {
+ const fixtureName = 'static/user_callout.html.raw';
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ Cookie.remove(USER_CALLOUT_COOKIE);
+
+ this.userCallout = new UserCallout();
+ this.closeButton = $('.close-user-callout');
+ this.userCalloutBtn = $('.user-callout-btn');
+ this.userCalloutContainer = $('.user-callout');
+ });
+
+ it('does not show when cookie is set not defined', () => {
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined();
+ expect(this.userCalloutContainer.is(':visible')).toBe(true);
+ });
+
+ it('shows when cookie is set to false', () => {
+ Cookie.set(USER_CALLOUT_COOKIE, 'false');
+
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined();
+ expect(this.userCalloutContainer.is(':visible')).toBe(true);
+ });
+
+ it('hides when user clicks on the dismiss-icon', () => {
+ this.closeButton.click();
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+ });
+
+ it('hides when user clicks on the "check it out" button', () => {
+ this.userCalloutBtn.click();
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+ });
+});
+
+describe('UserCallout when cookie is present', function () {
+ const fixtureName = 'static/user_callout.html.raw';
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ Cookie.set(USER_CALLOUT_COOKIE, 'true');
+ this.userCallout = new UserCallout();
+ this.userCalloutContainer = $('.user-callout');
+ });
+
+ it('removes the DOM element', () => {
+ expect(this.userCalloutContainer.length).toBe(0);
+ });
+});
diff --git a/spec/javascripts/version_check_image_spec.js b/spec/javascripts/version_check_image_spec.js
new file mode 100644
index 00000000000..464c1fce210
--- /dev/null
+++ b/spec/javascripts/version_check_image_spec.js
@@ -0,0 +1,33 @@
+const ClassSpecHelper = require('./helpers/class_spec_helper');
+const VersionCheckImage = require('~/version_check_image');
+require('jquery');
+
+describe('VersionCheckImage', function () {
+ describe('.bindErrorEvent', function () {
+ ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
+
+ beforeEach(function () {
+ this.imageElement = $('<div></div>');
+ });
+
+ it('registers an error event', function () {
+ spyOn($.prototype, 'on');
+ spyOn($.prototype, 'off').and.callFake(function () { return this; });
+
+ VersionCheckImage.bindErrorEvent(this.imageElement);
+
+ expect($.prototype.off).toHaveBeenCalledWith('error');
+ expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function));
+ });
+
+ it('hides the imageElement on error', function () {
+ spyOn($.prototype, 'hide');
+
+ VersionCheckImage.bindErrorEvent(this.imageElement);
+
+ this.imageElement.trigger('error');
+
+ expect($.prototype.hide).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js
index 9727c03c91e..9727c03c91e 100644
--- a/spec/javascripts/visibility_select_spec.js.es6
+++ b/spec/javascripts/visibility_select_spec.js
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js
new file mode 100644
index 00000000000..bc8e504c413
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+
+describe('Pipelines Async Button', () => {
+ let component;
+ let spy;
+ let AsyncButtonComponent;
+
+ beforeEach(() => {
+ AsyncButtonComponent = Vue.extend(asyncButtonComp);
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a button', () => {
+ expect(component.$el.tagName).toEqual('BUTTON');
+ });
+
+ it('should render the provided icon', () => {
+ expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
+ });
+
+ it('should render the provided title', () => {
+ expect(component.$el.getAttribute('title')).toContain('Foo');
+ expect(component.$el.getAttribute('aria-label')).toContain('Foo');
+ });
+
+ it('should render the provided cssClass', () => {
+ expect(component.$el.getAttribute('class')).toContain('bar');
+ });
+
+ it('should call the service when it is clicked with the provided endpoint', () => {
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ dataAttributes: {
+ 'data-foo': 'foo',
+ },
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+ });
+
+ describe('With confirm dialog', () => {
+ it('should call the service when confimation is positive', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ confirmActionMessage: 'bar',
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
new file mode 100644
index 00000000000..96a2a37b5f7
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+
+describe('Pipeline Url Component', () => {
+ let PipelineUrlComponent;
+
+ beforeEach(() => {
+ PipelineUrlComponent = Vue.extend(pipelineUrlComp);
+ });
+
+ it('should render a table cell', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.tagName).toEqual('TD');
+ });
+
+ it('should render a link the provided path and id', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
+ });
+
+ it('should render user information when a user is provided', () => {
+ const mockData = {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ user: {
+ web_url: '/',
+ name: 'foo',
+ avatar_url: '/',
+ },
+ },
+ };
+
+ const component = new PipelineUrlComponent({
+ propsData: mockData,
+ }).$mount();
+
+ const image = component.$el.querySelector('.js-pipeline-url-user img');
+
+ expect(
+ component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
+ ).toEqual(mockData.pipeline.user.web_url);
+ expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+ expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
+ });
+
+ it('should render "API" when no user is provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
+ });
+
+ it('should render latest, yaml invalid and stuck flags when provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ latest: true,
+ yaml_errors: true,
+ stuck: true,
+ },
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
+ expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
+ expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
new file mode 100644
index 00000000000..dba998c7688
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+
+describe('Pipelines Actions dropdown', () => {
+ let component;
+ let spy;
+ let actions;
+ let ActionsComponent;
+
+ beforeEach(() => {
+ ActionsComponent = Vue.extend(pipelinesActionsComp);
+
+ actions = [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1893/play',
+ },
+ ];
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(actions.length);
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(spy).toHaveBeenCalledWith(actions[0].path);
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
new file mode 100644
index 00000000000..f7f49649c1c
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+
+describe('Pipelines Artifacts dropdown', () => {
+ let component;
+ let artifacts;
+
+ beforeEach(() => {
+ const ArtifactsComponent = Vue.extend(artifactsComp);
+
+ artifacts = [
+ {
+ name: 'artifact',
+ path: '/download/path',
+ },
+ ];
+
+ component = new ArtifactsComponent({
+ propsData: {
+ artifacts,
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided artifacts', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(artifacts.length);
+ });
+
+ it('should render a link with the provided path', () => {
+ expect(
+ component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+ ).toEqual(artifacts[0].path);
+
+ expect(
+ component.$el.querySelector('.dropdown-menu li a span').textContent,
+ ).toContain(artifacts[0].name);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
new file mode 100644
index 00000000000..5c0934404bb
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
@@ -0,0 +1,72 @@
+import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+
+describe('Pipelines Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ });
+
+ it('should be initialized with an empty state', () => {
+ expect(store.state.pipelines).toEqual([]);
+ expect(store.state.count).toEqual({});
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ describe('storePipelines', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePipelines();
+ expect(store.state.pipelines).toEqual([]);
+ });
+
+ it('should store the provided array', () => {
+ const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
+ store.storePipelines(array);
+ expect(store.state.pipelines).toEqual(array);
+ });
+ });
+
+ describe('storeCount', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storeCount();
+ expect(store.state.count).toEqual({});
+ });
+
+ it('should store the provided count', () => {
+ const count = { all: 20, finished: 10 };
+ store.storeCount(count);
+
+ expect(store.state.count).toEqual(count);
+ });
+ });
+
+ describe('storePagination', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePagination();
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ it('should store pagination information normalized and parsed', () => {
+ const pagination = {
+ 'X-nExt-pAge': '2',
+ 'X-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '2',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ };
+
+ const expectedResult = {
+ perPage: 1,
+ page: 1,
+ total: 37,
+ totalPages: 2,
+ nextPage: 2,
+ previousPage: 2,
+ };
+
+ store.storePagination(pagination);
+ expect(store.state.pageInfo).toEqual(expectedResult);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
new file mode 100644
index 00000000000..df547299d75
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -0,0 +1,130 @@
+import Vue from 'vue';
+import commitComp from '~/vue_shared/components/commit';
+
+describe('Commit component', () => {
+ let props;
+ let component;
+ let CommitComponent;
+
+ beforeEach(() => {
+ CommitComponent = Vue.extend(commitComp);
+ });
+
+ it('should render a code-fork icon if it does not represent a tag', () => {
+ component = new CommitComponent({
+ propsData: {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
+ });
+
+ describe('Given all the props', () => {
+ beforeEach(() => {
+ props = {
+ tag: true,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ commitIconSvg: '<svg></svg>',
+ };
+
+ component = new CommitComponent({
+ propsData: props,
+ }).$mount();
+ });
+
+ it('should render a tag icon if it represents a tag', () => {
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
+ });
+
+ it('should render a link to the ref url', () => {
+ expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ });
+
+ it('should render the ref name', () => {
+ expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
+ });
+
+ it('should render the commit short sha with a link to the commit url', () => {
+ expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ });
+
+ it('should render the given commitIconSvg', () => {
+ expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
+ });
+
+ describe('Given commit title and author props', () => {
+ it('should render a link to the author profile', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
+ ).toEqual(props.author.web_url);
+ });
+
+ it('Should render the author avatar with title and alt attributes', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
+ ).toContain(props.author.username);
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
+ ).toContain(`${props.author.username}'s avatar`);
+ });
+ });
+
+ it('should render the commit title', () => {
+ expect(
+ component.$el.querySelector('a.commit-row-message').getAttribute('href'),
+ ).toEqual(props.commitUrl);
+ expect(
+ component.$el.querySelector('a.commit-row-message').textContent,
+ ).toContain(props.title);
+ });
+ });
+
+ describe('When commit title is not provided', () => {
+ it('should render default message', () => {
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = new CommitComponent({
+ propsData: props,
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('.commit-title span').textContent,
+ ).toContain('Cant find HEAD commit for this branch');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6
deleted file mode 100644
index 15ab10b9b69..00000000000
--- a/spec/javascripts/vue_shared/components/commit_spec.js.es6
+++ /dev/null
@@ -1,131 +0,0 @@
-require('~/vue_shared/components/commit');
-
-describe('Commit component', () => {
- let props;
- let component;
-
- it('should render a code-fork icon if it does not represent a tag', () => {
- setFixtures('<div class="test-commit-container"></div>');
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- username: 'jschatz1',
- },
- },
- });
-
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
- });
-
- describe('Given all the props', () => {
- beforeEach(() => {
- setFixtures('<div class="test-commit-container"></div>');
-
- props = {
- tag: true,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- username: 'jschatz1',
- },
- commitIconSvg: '<svg></svg>',
- };
-
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: props,
- });
- });
-
- it('should render a tag icon if it represents a tag', () => {
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
- });
-
- it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
- });
-
- it('should render the ref name', () => {
- expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
- });
-
- it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
- });
-
- it('should render the given commitIconSvg', () => {
- expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
- });
-
- describe('Given commit title and author props', () => {
- it('should render a link to the author profile', () => {
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
- ).toEqual(props.author.web_url);
- });
-
- it('Should render the author avatar with title and alt attributes', () => {
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
- ).toContain(props.author.username);
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
- ).toContain(`${props.author.username}'s avatar`);
- });
- });
-
- it('should render the commit title', () => {
- expect(
- component.$el.querySelector('a.commit-row-message').getAttribute('href'),
- ).toEqual(props.commitUrl);
- expect(
- component.$el.querySelector('a.commit-row-message').textContent,
- ).toContain(props.title);
- });
- });
-
- describe('When commit title is not provided', () => {
- it('should render default message', () => {
- setFixtures('<div class="test-commit-container"></div>');
- props = {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: null,
- author: {},
- };
-
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: props,
- });
-
- expect(
- component.$el.querySelector('.commit-title span').textContent,
- ).toContain('Cant find HEAD commit for this branch');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
new file mode 100644
index 00000000000..699625cdbb7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -0,0 +1,87 @@
+import Vue from 'vue';
+import tableRowComp from '~/vue_shared/components/pipelines_table_row';
+import pipeline from '../../commit/pipelines/mock_data';
+
+describe('Pipelines Table Row', () => {
+ let component;
+
+ beforeEach(() => {
+ const PipelinesTableRowComponent = Vue.extend(tableRowComp);
+
+ component = new PipelinesTableRowComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipeline,
+ service: {},
+ },
+ }).$mount();
+ });
+
+ it('should render a table row', () => {
+ expect(component.$el).toEqual('TR');
+ });
+
+ describe('status column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render status text', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').textContent,
+ ).toContain(pipeline.details.status.text);
+ });
+ });
+
+ describe('information column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render pipeline ID', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a > span').textContent,
+ ).toEqual(`#${pipeline.id}`);
+ });
+
+ describe('when a user is provided', () => {
+ it('should render user information', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ ).toEqual(pipeline.user.web_url);
+
+ expect(
+ component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
+ ).toEqual(pipeline.user.name);
+ });
+ });
+ });
+
+ describe('commit column', () => {
+ it('should render link to commit', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
+ ).toEqual(pipeline.commit.commit_path);
+ });
+ });
+
+ describe('stages column', () => {
+ it('should render an icon for each stage', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
+ ).toEqual(pipeline.details.stages.length);
+ });
+ });
+
+ describe('actions column', () => {
+ it('should render the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(6) ul li').length,
+ ).toEqual(pipeline.details.manual_actions.length);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
deleted file mode 100644
index 412abfd5e41..00000000000
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
+++ /dev/null
@@ -1,87 +0,0 @@
-require('~/vue_shared/components/pipelines_table_row');
-const pipeline = require('../../commit/pipelines/mock_data');
-
-describe('Pipelines Table Row', () => {
- let component;
- preloadFixtures('static/environments/element.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
-
- component = new gl.pipelines.PipelinesTableRowComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- pipeline,
- svgs: {},
- },
- });
- });
-
- it('should render a table row', () => {
- expect(component.$el).toEqual('TR');
- });
-
- describe('status column', () => {
- it('should render a pipeline link', () => {
- expect(
- component.$el.querySelector('td.commit-link a').getAttribute('href'),
- ).toEqual(pipeline.path);
- });
-
- it('should render status text', () => {
- expect(
- component.$el.querySelector('td.commit-link a').textContent,
- ).toContain(pipeline.details.status.text);
- });
- });
-
- describe('information column', () => {
- it('should render a pipeline link', () => {
- expect(
- component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
- ).toEqual(pipeline.path);
- });
-
- it('should render pipeline ID', () => {
- expect(
- component.$el.querySelector('td:nth-child(2) a > span').textContent,
- ).toEqual(`#${pipeline.id}`);
- });
-
- describe('when a user is provided', () => {
- it('should render user information', () => {
- expect(
- component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
- ).toEqual(pipeline.user.web_url);
-
- expect(
- component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
- ).toEqual(pipeline.user.name);
- });
- });
- });
-
- describe('commit column', () => {
- it('should render link to commit', () => {
- expect(
- component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
- ).toEqual(pipeline.commit.commit_path);
- });
- });
-
- describe('stages column', () => {
- it('should render an icon for each stage', () => {
- expect(
- component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
- ).toEqual(pipeline.details.stages.length);
- });
- });
-
- describe('actions column', () => {
- it('should render the provided actions', () => {
- expect(
- component.$el.querySelectorAll('td:nth-child(6) ul li').length,
- ).toEqual(pipeline.details.manual_actions.length);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
new file mode 100644
index 00000000000..b0b1df5a753
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
+import '~/lib/utils/datetime_utility';
+import pipeline from '../../commit/pipelines/mock_data';
+
+describe('Pipelines Table', () => {
+ let PipelinesTableComponent;
+
+ beforeEach(() => {
+ PipelinesTableComponent = Vue.extend(pipelinesTableComp);
+ });
+
+ describe('table', () => {
+ let component;
+ beforeEach(() => {
+ component = new PipelinesTableComponent({
+ propsData: {
+ pipelines: [],
+ service: {},
+ },
+ }).$mount();
+ });
+
+ it('should render a table', () => {
+ expect(component.$el).toEqual('TABLE');
+ });
+
+ it('should render table head with correct columns', () => {
+ expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status');
+ expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline');
+ expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit');
+ expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages');
+ expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual('');
+ expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual('');
+ });
+ });
+
+ describe('without data', () => {
+ it('should render an empty table', () => {
+ const component = new PipelinesTableComponent({
+ propsData: {
+ pipelines: [],
+ service: {},
+ },
+ }).$mount();
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
+ });
+ });
+
+ describe('with data', () => {
+ it('should render rows', () => {
+ const component = new PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [pipeline],
+ service: {},
+ },
+ }).$mount();
+
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
deleted file mode 100644
index 54d81e2ea7d..00000000000
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
+++ /dev/null
@@ -1,64 +0,0 @@
-require('~/vue_shared/components/pipelines_table');
-require('~/lib/utils/datetime_utility');
-const pipeline = require('../../commit/pipelines/mock_data');
-
-describe('Pipelines Table', () => {
- preloadFixtures('static/environments/element.html.raw');
-
- beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
- });
-
- describe('table', () => {
- let component;
- beforeEach(() => {
- component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- pipelines: [],
- svgs: {},
- },
- });
- });
-
- it('should render a table', () => {
- expect(component.$el).toEqual('TABLE');
- });
-
- it('should render table head with correct columns', () => {
- expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status');
- expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline');
- expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit');
- expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages');
- expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual('');
- expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual('');
- });
- });
-
- describe('without data', () => {
- it('should render an empty table', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- pipelines: [],
- svgs: {},
- },
- });
- expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
- });
- });
-
- describe('with data', () => {
- it('should render rows', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- pipelines: [pipeline],
- svgs: {},
- },
- });
-
- expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
new file mode 100644
index 00000000000..a5c3870b3ac
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -0,0 +1,146 @@
+import Vue from 'vue';
+import paginationComp from '~/vue_shared/components/table_pagination';
+import '~/lib/utils/common_utils';
+
+describe('Pagination component', () => {
+ let component;
+ let PaginationComponent;
+
+ const changeChanges = {
+ one: '',
+ };
+
+ const change = (one) => {
+ changeChanges.one = one;
+ };
+
+ beforeEach(() => {
+ PaginationComponent = Vue.extend(paginationComp);
+ });
+
+ it('should render and start at page 1', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ }).$mount();
+
+ expect(component.$el.classList).toContain('gl-pagination');
+
+ component.changePage({ target: { innerText: '1' } });
+
+ expect(changeChanges.one).toEqual(1);
+ });
+
+ it('should go to the previous page', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 3,
+ previousPage: 1,
+ },
+ change,
+ },
+ }).$mount();
+
+ component.changePage({ target: { innerText: 'Prev' } });
+
+ expect(changeChanges.one).toEqual(1);
+ });
+
+ it('should go to the next page', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ }).$mount();
+
+ component.changePage({ target: { innerText: 'Next' } });
+
+ expect(changeChanges.one).toEqual(5);
+ });
+
+ it('should go to the last page', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ }).$mount();
+
+ component.changePage({ target: { innerText: 'Last >>' } });
+
+ expect(changeChanges.one).toEqual(10);
+ });
+
+ it('should go to the first page', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ }).$mount();
+
+ component.changePage({ target: { innerText: '<< First' } });
+
+ expect(changeChanges.one).toEqual(1);
+ });
+
+ it('should do nothing', () => {
+ component = new PaginationComponent({
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ }).$mount();
+
+ component.changePage({ target: { innerText: '...' } });
+
+ expect(changeChanges.one).toEqual(1);
+ });
+});
+
+describe('paramHelper', () => {
+ it('can parse url parameters correctly', () => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual('all');
+ expect(p).toEqual('2');
+ });
+
+ it('returns null if param not in url', () => {
+ window.history.pushState({}, null, '?p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual(null);
+ expect(p).toEqual('2');
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
deleted file mode 100644
index dd495cb43bc..00000000000
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
+++ /dev/null
@@ -1,166 +0,0 @@
-require('~/lib/utils/common_utils');
-require('~/vue_shared/components/table_pagination');
-
-describe('Pagination component', () => {
- let component;
-
- const changeChanges = {
- one: '',
- two: '',
- };
-
- const change = (one, two) => {
- changeChanges.one = one;
- changeChanges.two = two;
- };
-
- it('should render and start at page 1', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 2,
- previousPage: '',
- },
- change,
- },
- });
-
- expect(component.$el.classList).toContain('gl-pagination');
-
- component.changePage({ target: { innerText: '1' } });
-
- expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
- });
-
- it('should go to the previous page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 3,
- previousPage: 1,
- },
- change,
- },
- });
-
- component.changePage({ target: { innerText: 'Prev' } });
-
- expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
- });
-
- it('should go to the next page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 5,
- previousPage: 3,
- },
- change,
- },
- });
-
- component.changePage({ target: { innerText: 'Next' } });
-
- expect(changeChanges.one).toEqual(5);
- expect(changeChanges.two).toEqual(null);
- });
-
- it('should go to the last page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 5,
- previousPage: 3,
- },
- change,
- },
- });
-
- component.changePage({ target: { innerText: 'Last >>' } });
-
- expect(changeChanges.one).toEqual(10);
- expect(changeChanges.two).toEqual(null);
- });
-
- it('should go to the first page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 5,
- previousPage: 3,
- },
- change,
- },
- });
-
- component.changePage({ target: { innerText: '<< First' } });
-
- expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
- });
-
- it('should do nothing', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
- propsData: {
- pageInfo: {
- totalPages: 10,
- nextPage: 2,
- previousPage: '',
- },
- change,
- },
- });
-
- component.changePage({ target: { innerText: '...' } });
-
- expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
- });
-});
-
-describe('paramHelper', () => {
- it('can parse url parameters correctly', () => {
- window.history.pushState({}, null, '?scope=all&p=2');
-
- const scope = gl.utils.getParameterByName('scope');
- const p = gl.utils.getParameterByName('p');
-
- expect(scope).toEqual('all');
- expect(p).toEqual('2');
- });
-
- it('returns null if param not in url', () => {
- window.history.pushState({}, null, '?p=2');
-
- const scope = gl.utils.getParameterByName('scope');
- const p = gl.utils.getParameterByName('p');
-
- expect(scope).toEqual(null);
- expect(p).toEqual('2');
- });
-});
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index c8e62f528df..707212e07fd 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'replaces supported unicode emoji' do
doc = filter('<p>❤️</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'ignores unsupported emoji' do
@@ -30,152 +30,78 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'correctly encodes the URL' do
doc = filter('<p>:+1:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'correctly encodes unicode to the URL' do
doc = filter('<p>👍</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'matches at the start of a string' do
doc = filter(':+1:')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the start of a string' do
doc = filter("'👍'")
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches at the end of a string' do
doc = filter('This gets a :-1:')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the end of a string' do
doc = filter('This gets a 👍')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches with adjacent text' do
doc = filter('+1 (👍)')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
- expect(doc.css('img').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 3
end
it 'unicode matches multiple emoji in a row' do
doc = filter("'🙈🙉🙊'")
- expect(doc.css('img').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 3
end
it 'mixed matches multiple emoji in a row' do
doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
- expect(doc.css('img').size).to eq 6
+ expect(doc.css('gl-emoji').size).to eq 6
end
- it 'has a title attribute' do
+ it 'has a data-name attribute' do
doc = filter(':-1:')
- expect(doc.css('img').first.attr('title')).to eq ':-1:'
+ expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
end
- it 'unicode has a title attribute' do
- doc = filter("'👎'")
- expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
- end
-
- it 'has an alt attribute' do
+ it 'has a data-unicode-version attribute' do
doc = filter(':-1:')
- expect(doc.css('img').first.attr('alt')).to eq ':-1:'
- end
-
- it 'unicode has an alt attribute' do
- doc = filter("'👎'")
- expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
- end
-
- it 'has an align attribute' do
- doc = filter(':8ball:')
- expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
- end
-
- it 'unicode has an align attribute' do
- doc = filter("'🎱'")
- expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
- end
-
- it 'has an emoji class' do
- doc = filter(':cat:')
- expect(doc.css('img').first.attr('class')).to eq 'emoji'
- end
-
- it 'unicode has an emoji class' do
- doc = filter("'🐱'")
- expect(doc.css('img').first.attr('class')).to eq 'emoji'
- end
-
- it 'has height and width attributes' do
- doc = filter(':dog:')
- img = doc.css('img').first
-
- expect(img.attr('width')).to eq '20'
- expect(img.attr('height')).to eq '20'
- end
-
- it 'unicode has height and width attributes' do
- doc = filter("'🐶'")
- img = doc.css('img').first
-
- expect(img.attr('width')).to eq '20'
- expect(img.attr('height')).to eq '20'
+ expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
- expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'unicode keeps whitespace intact' do
doc = filter('This deserves a 🎱, big time.')
- expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
- end
-
- it 'uses a custom asset_root context' do
- root = Gitlab.config.gitlab.url + 'gitlab/root'
-
- doc = filter(':smile:', asset_root: root)
- expect(doc.css('img').first.attr('src')).to start_with(root)
- end
-
- it 'uses a custom asset_host context' do
- ActionController::Base.asset_host = 'https://cdn.example.com'
-
- doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
- expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
- end
-
- it 'uses a custom asset_root context' do
- root = Gitlab.config.gitlab.url + 'gitlab/root'
-
- doc = filter("'🎱'", asset_root: root)
- expect(doc.css('img').first.attr('src')).to start_with(root)
- end
-
- it 'uses a custom asset_host context' do
- ActionController::Base.asset_host = 'https://cdn.example.com'
-
- doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
- expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
end
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index a2a1ed58d1b..294558b3db2 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
end
it 'does not wrap a duplicate link' do
- exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)
- expect(filter(act).to_html).to eq exp
+ doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
+ expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
end
it 'works with external images' do
@@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end
- it 'wraps the image with a link and a div' do
- doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.to_html).to include('<div class="image-container">')
+ it 'works with inline images' do
+ doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
+ expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
end
end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index b38e3b17e64..b4cd5f63a15 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -86,6 +86,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
+ it 'allows `summary` elements' do
+ exp = act = '<summary>summary line</summary>'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows `details` elements' do
+ exp = act = '<details>long text goes here</details>'
+ expect(filter(act).to_html).to eq exp
+ end
+
it 'removes `rel` attribute from `a` elements' do
act = %q{<a href="#" rel="nofollow">Link</a>}
exp = %q{<a href="#">Link</a>}
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 69e3c52b35a..63fb1bb25c4 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index d5d128c1907..9873774909e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -123,6 +123,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
end
+
+ it 'has the full group name as a title' do
+ doc = reference_filter("Hey #{reference}")
+
+ expect(doc.css('a').first.attr('title')).to eq group.full_name
+ end
end
it 'links with adjacent text' do
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
index 015a7f80e03..9008cb3e870 100644
--- a/spec/lib/bitbucket/collection_spec.rb
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -19,6 +19,6 @@ describe Bitbucket::Collection do
it "iterates paginator" do
collection = described_class.new(TestPaginator.new)
- expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
+ expect(collection.to_a).to match(%w(result_1_page_1 result_2_page_1 result_1_page_2 result_2_page_2))
end
end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index adcd978e1b3..405265cc669 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -29,7 +29,7 @@ describe Bitbucket::Representation::Repo do
end
describe '#owner_and_slug' do
- it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) }
+ it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(%w(ben test)) }
end
describe '#owner' do
@@ -42,7 +42,7 @@ describe Bitbucket::Representation::Repo do
describe '#clone_url' do
it 'builds url' do
- data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
+ data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git')
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 68ad429608d..53abc056602 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -15,9 +15,9 @@ module Ci
end
describe '#build_attributes' do
- describe 'coverage entry' do
- subject { described_class.new(config, path).build_attributes(:rspec) }
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+ describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
@@ -30,6 +30,56 @@ module Ci
end
end
end
+
+ describe 'allow failure entry' do
+ context 'when job is a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual' })
+ end
+
+ it 'is allowed to fail' do
+ expect(subject[:allow_failure]).to be true
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+ end
+ end
end
describe "#builds_for_ref" do
@@ -96,7 +146,7 @@ module Ci
it "returns builds if only has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+ rspec: { script: "rspec", type: type, only: %w(master deploy) }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -173,8 +223,8 @@ module Ci
it "returns build only for specified type" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
- staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ rspec: { script: "rspec", type: "test", only: %w(master deploy) },
+ staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
})
@@ -252,7 +302,7 @@ module Ci
it "does not return builds if except has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["master", "deploy"] }
+ rspec: { script: "rspec", type: type, except: %w(master deploy) }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -580,7 +630,7 @@ module Ci
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
let(:variables) do
- [ 'VAR1', 'value1', 'VAR2', 'value2' ]
+ %w(VAR1 value1 VAR2 value2)
end
it 'raises error' do
@@ -909,7 +959,7 @@ module Ci
end
context 'dependencies to builds' do
- let(:dependencies) { ['build1', 'build2'] }
+ let(:dependencies) { %w(build1 build2) }
it { expect { subject }.not_to raise_error }
end
@@ -1215,7 +1265,7 @@ EOT
end
it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
@@ -1257,42 +1307,42 @@ EOT
end
it "returns errors if job artifacts:name is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
end
it "returns errors if job artifacts:when is not an a predefined value" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
end
it "returns errors if job artifacts:expire_in is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:expire_in is not an a valid duration" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
@@ -1320,28 +1370,28 @@ EOT
end
it "returns errors if job cache:key is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index a5251e9a8c2..4f25ad88960 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -6,7 +6,7 @@ describe ProjectUrlConstrainer, lib: true do
describe '#matches?' do
context 'valid request' do
- let(:request) { build_request(namespace.path, project.path) }
+ let(:request) { build_request(namespace.full_path, project.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
@@ -19,7 +19,7 @@ describe ProjectUrlConstrainer, lib: true do
end
context "project id ending with .git" do
- let(:request) { build_request(namespace.path, project.path + '.git') }
+ let(:request) { build_request(namespace.full_path, project.path + '.git') }
it { expect(subject.matches?(request)).to be_falsey }
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90bc7dad379..90628917943 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -7,58 +7,49 @@ describe ExpandVariables do
tests = [
{ value: 'key',
result: 'key',
- variables: []
- },
+ variables: [] },
{ value: 'key$variable',
result: 'key',
- variables: []
- },
+ variables: [] },
{ value: 'key$variable',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
- ]
- },
+ ] },
{ value: 'key${variable}',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
- ]
- },
+ ] },
{ value: 'key$variable$variable2',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
- ]
- },
+ ] },
{ value: 'key${variable}${variable2}',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
- ]
- },
+ ] },
{ value: 'key$variable2$variable',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
- ]
- },
+ ] },
{ value: 'key${variable2}${variable}',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
- ]
- },
- { value: 'review/$CI_BUILD_REF_NAME',
+ ] },
+ { value: 'review/$CI_COMMIT_REF_NAME',
result: 'review/feature/add-review-apps',
variables: [
- { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
- ]
- },
+ { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
+ ] },
]
tests.each do |test|
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 29c07655ae8..33ab005667a 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -177,12 +177,12 @@ describe ExtractsPath, lib: true do
it "extracts a valid commit SHA" do
expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
- ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG']
+ %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
)
end
it "falls back to a primitive split for an invalid ref" do
- expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG'])
+ expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
end
end
end
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb
index dc57e94f193..cc8daa535d6 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/git_ref_validator_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy }
+ it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey }
@@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey }
end
diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
new file mode 100644
index 00000000000..94dcddcc30c
--- /dev/null
+++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do
+ include_context 'unique ips sign in limit'
+ let(:user) { create(:user) }
+
+ describe '#count_unique_ips' do
+ context 'non unique IPs' do
+ it 'properly counts them' do
+ expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1)
+ expect(described_class.update_and_return_ips_count(user.id, 'ip1')).to eq(1)
+ end
+ end
+
+ context 'unique IPs' do
+ it 'properly counts them' do
+ expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
+ expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
+ end
+ end
+
+ it 'resets count after specified time window' do
+ Timecop.freeze do
+ expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
+ expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
+
+ Timecop.travel(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
+ expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1)
+ expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe '#limit_user!' do
+ include_examples 'user login operation with unique ip limit' do
+ def operation
+ described_class.limit_user! { user }
+ end
+ end
+
+ context 'allow 2 unique ips' do
+ before { current_application_settings.update!(unique_ips_limit_per_user: 2) }
+
+ it 'blocks user trying to login from third ip' do
+ change_ip('ip1')
+ expect(described_class.limit_user! { user }).to eq(user)
+
+ change_ip('ip2')
+ expect(described_class.limit_user! { user }).to eq(user)
+
+ change_ip('ip3')
+ expect { described_class.limit_user! { user } }.to raise_error(Gitlab::Auth::TooManyIps)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b234de4c772..03c4879ed6f 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -3,6 +3,24 @@ require 'spec_helper'
describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class }
+ describe 'constants' do
+ it 'API_SCOPES contains all scopes for API access' do
+ expect(subject::API_SCOPES).to eq [:api, :read_user]
+ end
+
+ it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
+ expect(subject::OPENID_SCOPES).to eq [:openid]
+ end
+
+ it 'DEFAULT_SCOPES contains all default scopes' do
+ expect(subject::DEFAULT_SCOPES).to eq [:api]
+ end
+
+ it 'OPTIONAL_SCOPES contains all non-default scopes' do
+ expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid]
+ end
+ end
+
describe 'find_for_git_client' do
context 'build token' do
subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
@@ -58,6 +76,14 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
+ include_examples 'user login operation with unique ip limit' do
+ let(:user) { create(:user, password: 'password') }
+
+ def operation
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+ end
+
context 'while using LFS authenticate' do
it 'recognizes user lfs tokens' do
user = create(:user)
@@ -110,25 +136,37 @@ describe Gitlab::Auth, lib: true do
end
context 'while using personal access tokens as passwords' do
- let(:user) { create(:user) }
- let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
-
it 'succeeds for personal access tokens with the `api` scope' do
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ personal_access_token = create(:personal_access_token, scopes: ['api'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
+ end
+
+ it 'succeeds if it is an impersonation token' do
+ impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities))
end
it 'fails for personal access tokens with other scopes' do
- personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ personal_access_token = create(:personal_access_token, scopes: ['read_user'])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
- it 'does not try password auth before personal access tokens' do
- expect(gl_auth).not_to receive(:find_with_user_password)
+ it 'fails for impersonation token with other scopes' do
+ impersonation_token = create(:personal_access_token, scopes: ['read_user'])
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ end
- gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+ it 'fails if password is nil' do
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
+ expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
end
@@ -196,6 +234,24 @@ describe Gitlab::Auth, lib: true do
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
+ include_examples 'user login operation with unique ip limit' do
+ def operation
+ expect(gl_auth.find_with_user_password(username, password)).to eq(user)
+ end
+ end
+
+ it "does not find user in blocked state" do
+ user.block
+
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ end
+
+ it "does not find user in ldap_blocked state" do
+ user.ldap_block
+
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ end
+
context "with ldap enabled" do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
diff --git a/spec/lib/gitlab/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
deleted file mode 100644
index 00a110e31f8..00000000000
--- a/spec/lib/gitlab/award_emoji_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AwardEmoji do
- describe '.urls' do
- after do
- Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
- end
-
- subject { Gitlab::AwardEmoji.urls }
-
- it { is_expected.to be_an_instance_of(Array) }
- it { is_expected.not_to be_empty }
-
- context 'every Hash in the Array' do
- it 'has the correct keys and values' do
- subject.each do |hash|
- expect(hash[:name]).to be_an_instance_of(String)
- expect(hash[:path]).to be_an_instance_of(String)
- end
- end
- end
-
- context 'handles relative root' do
- it 'includes the full path' do
- allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
-
- subject.each do |hash|
- expect(hash[:name]).to be_an_instance_of(String)
- expect(hash[:path]).to start_with('/gitlab')
- end
- end
- end
- end
-
- describe '.emoji_by_category' do
- it "only contains known categories" do
- undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
- expect(undefined_categories).to be_empty
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 0a2fe5af2c3..a7ee7f53a6b 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -87,10 +87,10 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
body: issues_statuses_sample_data.to_json)
stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
- with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
- to_return(status: 200,
- body: "",
- headers: {})
+ with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
+ to_return(status: 200,
+ body: "",
+ headers: {})
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
index 5b678d31fce..3916fc704a4 100644
--- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -26,6 +26,21 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do
end
end
+ context 'with labels' do
+ let(:label) { create(:label, project: project, title: 'mep') }
+ let(:label1) { create(:label, project: project, title: 'mop') }
+
+ before do
+ issue.labels << [label, label1]
+ end
+
+ it 'shows the labels' do
+ labels = attachment[:fields].find { |f| f[:title] == 'Labels' }
+
+ expect(labels[:value]).to eq("mep, mop")
+ end
+ end
+
context 'confidential issue' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index cadfbadca10..e22f88b7a32 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -12,8 +12,16 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
ref: 'refs/heads/master'
}
end
-
- subject { described_class.new(changes, project: project, user_access: user_access).exec }
+ let(:protocol) { 'ssh' }
+
+ subject do
+ described_class.new(
+ changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol
+ ).exec
+ end
before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb
new file mode 100644
index 00000000000..382385dfd6b
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/image_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Image do
+ let(:job) { create(:ci_build, :no_options) }
+
+ describe '#from_image' do
+ subject { described_class.from_image(job) }
+
+ context 'when image is defined in job' do
+ let(:image_name) { 'ruby:2.1' }
+ let(:job) { create(:ci_build, options: { image: image_name } ) }
+
+ it 'fabricates an object of the proper class' do
+ is_expected.to be_kind_of(described_class)
+ end
+
+ it 'populates fabricated object with the proper name attribute' do
+ expect(subject.name).to eq(image_name)
+ end
+
+ context 'when image name is empty' do
+ let(:image_name) { '' }
+
+ it 'does not fabricate an object' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when image is not defined in job' do
+ it 'does not fabricate an object' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#from_services' do
+ subject { described_class.from_services(job) }
+
+ context 'when services are defined in job' do
+ let(:service_image_name) { 'postgres' }
+ let(:job) { create(:ci_build, options: { services: [service_image_name] }) }
+
+ it 'fabricates an non-empty array of objects' do
+ is_expected.to be_kind_of(Array)
+ is_expected.not_to be_empty
+ expect(subject.first.name).to eq(service_image_name)
+ end
+
+ context 'when service image name is empty' do
+ let(:service_image_name) { '' }
+
+ it 'fabricates an empty array' do
+ is_expected.to be_kind_of(Array)
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ context 'when services are not defined in job' do
+ it 'fabricates an empty array' do
+ is_expected.to be_kind_of(Array)
+ is_expected.to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb
new file mode 100644
index 00000000000..2a314a744ca
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/step_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Step do
+ let(:job) { create(:ci_build, :no_options, commands: "ls -la\ndate") }
+
+ describe '#from_commands' do
+ subject { described_class.from_commands(job) }
+
+ it 'fabricates an object' do
+ expect(subject.name).to eq(:script)
+ expect(subject.script).to eq(['ls -la', 'date'])
+ expect(subject.timeout).to eq(job.timeout)
+ expect(subject.when).to eq('on_success')
+ expect(subject.allow_failure).to be_falsey
+ end
+ end
+
+ describe '#from_after_script' do
+ subject { described_class.from_after_script(job) }
+
+ context 'when after_script is empty' do
+ it 'doesn not fabricate an object' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when after_script is not empty' do
+ let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) }
+
+ it 'fabricates an object' do
+ expect(subject.name).to eq(:after_script)
+ expect(subject.script).to eq(['ls -la', 'date'])
+ expect(subject.timeout).to eq(job.timeout)
+ expect(subject.when).to eq('always')
+ expect(subject.allow_failure).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 70a327c5183..2ed120f356a 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -24,6 +24,20 @@ describe Gitlab::Ci::Config::Entry::Cache do
expect(entry).to be_valid
end
end
+
+ context 'when key is missing' do
+ let(:config) do
+ { untracked: true,
+ paths: ['some/path/'] }
+ end
+
+ describe '#value' do
+ it 'sets key with the default' do
+ expect(entry.value[:key])
+ .to eq(Gitlab::Ci::Config::Entry::Key.default)
+ end
+ end
+ end
end
context 'when entry value is not correct' do
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index b8b0825a1c7..afa4a089418 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array' do
- let(:config) { ['ls', 'pwd'] }
+ let(:config) { %w(ls pwd) }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 2adbed2154f..c330e609337 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -151,8 +151,8 @@ describe Gitlab::Ci::Config::Entry::Environment do
context 'when variables are used for environment' do
let(:config) do
- { name: 'review/$CI_BUILD_REF_NAME',
- url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+ { name: 'review/$CI_COMMIT_REF_NAME',
+ url: 'https://$CI_COMMIT_REF_NAME.review.gitlab.com' }
end
describe '#valid?' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
index 00dad5d9591..8dd48e4efae 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -8,20 +8,20 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.create!
- expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.value).to eq %w(ls pwd)
end
context 'when setting description' do
it 'creates entry with description' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.with(description: 'test description')
.create!
- expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.value).to eq %w(ls pwd)
expect(entry.description).to eq 'test description'
end
end
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.with(key: 'test key')
.create!
@@ -60,13 +60,13 @@ describe Gitlab::Ci::Config::Entry::Factory do
end
context 'when creating entry with nil value' do
- it 'creates an undefined entry' do
+ it 'creates an unspecified entry' do
entry = factory
.value(nil)
.create!
expect(entry)
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ .not_to be_specified
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 432a99dce33..684d01e9056 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -21,12 +21,12 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when configuration is valid' do
context 'when some entries defined' do
let(:hash) do
- { before_script: ['ls', 'pwd'],
+ { before_script: %w(ls pwd),
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'value' },
after_script: ['make clean'],
- stages: ['build', 'pages'],
+ stages: %w(build pages),
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' } }
@@ -89,7 +89,7 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#before_script_value' do
it 'returns correct script' do
- expect(global.before_script_value).to eq ['ls', 'pwd']
+ expect(global.before_script_value).to eq %w(ls pwd)
end
end
@@ -126,7 +126,7 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when deprecated types key defined' do
let(:hash) do
- { types: ['test', 'deploy'],
+ { types: %w(test deploy),
rspec: { script: 'rspec' } }
end
@@ -148,13 +148,14 @@ describe Gitlab::Ci::Config::Entry::Global do
expect(global.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- before_script: ['ls', 'pwd'],
+ before_script: %w(ls pwd),
commands: "ls\npwd\nrspec\nls",
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: { VAR: 'value' },
+ ignore: false,
after_script: ['make clean'] },
spinach: { name: :spinach,
before_script: [],
@@ -165,6 +166,7 @@ describe Gitlab::Ci::Config::Entry::Global do
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: {},
+ ignore: false,
after_script: ['make clean'] },
)
end
@@ -186,7 +188,7 @@ describe Gitlab::Ci::Config::Entry::Global do
it 'contains unspecified nodes' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ .not_to be_specified
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index d20f4ec207d..9249bb9c172 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -144,6 +144,7 @@ describe Gitlab::Ci::Config::Entry::Job do
script: %w[rspec],
commands: "ls\npwd\nrspec",
stage: 'test',
+ ignore: false,
after_script: %w[cleanup])
end
end
@@ -159,4 +160,82 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
end
+
+ describe '#manual_action?' do
+ context 'when job is a manual action' do
+ let(:config) { { script: 'deploy', when: 'manual' } }
+
+ it 'is a manual action' do
+ expect(entry).to be_manual_action
+ end
+ end
+
+ context 'when job is not a manual action' do
+ let(:config) { { script: 'deploy' } }
+
+ it 'is not a manual action' do
+ expect(entry).not_to be_manual_action
+ end
+ end
+ end
+
+ describe '#ignored?' do
+ context 'when job is a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual' }
+ end
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: true }
+ end
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: false }
+ end
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) { { script: 'deploy' } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: true } }
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: false } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index aaebf783962..7d104372ac6 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -62,10 +62,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do
rspec: { name: :rspec,
script: %w[rspec],
commands: 'rspec',
+ ignore: false,
stage: 'test' },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
+ ignore: false,
stage: 'test' })
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index a55e5b4b8ac..5d4de60bc8a 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Key do
end
context 'when entry value is not correct' do
- let(:config) { [ 'incorrect' ] }
+ let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
@@ -31,4 +31,10 @@ describe Gitlab::Ci::Config::Entry::Key do
end
end
end
+
+ describe '.default' do
+ it 'returns default key' do
+ expect(described_class.default).to eq 'default'
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
index e60c9aaf661..1d9c5ddee9b 100644
--- a/spec/lib/gitlab/ci/config/entry/paths_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Paths do
end
context 'when entry value is not valid' do
- let(:config) { [ 1 ] }
+ let(:config) { [1] }
describe '#errors' do
it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
index aa99cee2690..069eaa26422 100644
--- a/spec/lib/gitlab/ci/config/entry/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Config::Entry::Script do
describe 'validations' do
context 'when entry config value is correct' do
- let(:config) { ['ls', 'pwd'] }
+ let(:config) { %w(ls pwd) }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 58327d08904..f15f02f403e 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Variables do
end
context 'when entry value is not correct' do
- let(:config) { [ :VAR, 'test' ] }
+ let(:config) { [:VAR, 'test'] }
describe '#errors' do
it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 0c40fca0c1a..8b3bd08cf13 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -192,7 +192,7 @@ describe Gitlab::Ci::Status::Build::Factory do
let(:build) { create(:ci_build, :playable) }
it 'matches correct core status' do
- expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
end
it 'matches correct extended statuses' do
@@ -200,12 +200,13 @@ describe Gitlab::Ci::Status::Build::Factory do
.to eq [Gitlab::Ci::Status::Build::Play]
end
- it 'fabricates a core skipped status' do
+ it 'fabricates a play detailed status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
+ expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
expect(status.label).to eq 'manual play action'
expect(status).to have_details
@@ -218,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
let(:build) { create(:ci_build, :playable, :teardown_environment) }
it 'matches correct core status' do
- expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
end
it 'matches correct extended statuses' do
@@ -226,12 +227,13 @@ describe Gitlab::Ci::Status::Build::Factory do
.to eq [Gitlab::Ci::Status::Build::Stop]
end
- it 'fabricates a core skipped status' do
+ it 'fabricates a stop detailed status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
+ expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
expect(status.label).to eq 'manual stop action'
expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f3e72ea1796..6c97a4fe5ca 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -6,22 +6,10 @@ describe Gitlab::Ci::Status::Build::Play do
subject { described_class.new(status) }
- describe '#text' do
- it { expect(subject.text).to eq 'manual' }
- end
-
describe '#label' do
it { expect(subject.label).to eq 'manual play action' }
end
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'manual' }
- end
-
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 41c2b624774..8d021c35a69 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -8,22 +8,10 @@ describe Gitlab::Ci::Status::Build::Stop do
described_class.new(status)
end
- describe '#text' do
- it { expect(subject.text).to eq 'manual' }
- end
-
describe '#label' do
it { expect(subject.label).to eq 'manual stop action' }
end
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'manual' }
- end
-
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 38412fe2e4f..768f8926f1d 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Canceled do
end
describe '#text' do
- it { expect(subject.label).to eq 'canceled' }
+ it { expect(subject.text).to eq 'canceled' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 6d847484693..e96c13aede3 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Created do
end
describe '#text' do
- it { expect(subject.label).to eq 'created' }
+ it { expect(subject.text).to eq 'created' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 990d686d22c..e5da0a91159 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Failed do
end
describe '#text' do
- it { expect(subject.label).to eq 'failed' }
+ it { expect(subject.text).to eq 'failed' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
new file mode 100644
index 00000000000..3fd3727b92d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Manual do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'manual' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 7bb6579c317..8d09cf2a05a 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Pending do
end
describe '#text' do
- it { expect(subject.label).to eq 'pending' }
+ it { expect(subject.text).to eq 'pending' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb
new file mode 100644
index 00000000000..1a2b952d374
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/pipeline/blocked_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Pipeline::Blocked do
+ let(:pipeline) { double('pipeline') }
+
+ subject do
+ described_class.new(pipeline)
+ end
+
+ describe '#text' do
+ it 'overrides status text' do
+ expect(subject.text).to eq 'blocked'
+ end
+ end
+
+ describe '#label' do
+ it 'overrides status label' do
+ expect(subject.label).to eq 'waiting for manual action'
+ end
+ end
+
+ describe '.matches?' do
+ let(:user) { double('user') }
+ subject { described_class.matches?(pipeline, user) }
+
+ context 'when pipeline is blocked' do
+ let(:pipeline) { create(:ci_pipeline, :blocked) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when pipeline is not blocked' do
+ let(:pipeline) { create(:ci_pipeline, :success) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index b10a447c27a..dd754b849b2 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end
context 'when pipeline has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ (HasStatus::AVAILABLE_STATUSES - [HasStatus::BLOCKED_STATUS])
+ .each do |simple_status|
context "when core status is #{simple_status}" do
let(:pipeline) { create(:ci_pipeline, status: simple_status) }
@@ -23,7 +24,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
expect(factory.core_status).to be_a expected_status
end
- it 'does not matche extended statuses' do
+ it 'does not match extended statuses' do
expect(factory.extended_statuses).to be_empty
end
@@ -39,6 +40,27 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end
end
end
+
+ context "when core status is manual" do
+ let(:pipeline) { create(:ci_pipeline, status: :manual) }
+
+ it "matches manual core status" do
+ expect(factory.core_status)
+ .to be_a Gitlab::Ci::Status::Manual
+ end
+
+ it 'matches a correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Pipeline::Blocked]
+ end
+
+ it 'extends core status with common pipeline methods' do
+ expect(status).to have_details
+ expect(status).not_to have_action
+ expect(status.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ end
+ end
end
context 'when pipeline has warnings' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 852d6c06baf..10d3bf749c1 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Running do
end
describe '#text' do
- it { expect(subject.label).to eq 'running' }
+ it { expect(subject.text).to eq 'running' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index e00b356a24b..10db93d3802 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Skipped do
end
describe '#text' do
- it { expect(subject.label).to eq 'skipped' }
+ it { expect(subject.text).to eq 'skipped' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 4a89e1faf40..230f24b94a4 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Success do
end
describe '#text' do
- it { expect(subject.label).to eq 'passed' }
+ it { expect(subject.text).to eq 'passed' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index fbf679c5215..780ac0ad97e 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Conflict::File, lib: true do
it 'returns a file containing only the chosen parts of the resolved sections' do
expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
- to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+ to eq(%w(both new both old both new both))
end
end
@@ -123,7 +123,7 @@ describe Gitlab::Conflict::File, lib: true do
it 'sets conflict to true for sections with only changed lines' do
conflict_file.sections.select { |section| section[:conflict] }.each do |section|
section[:lines].each do |line|
- expect(line.type).to be_in(['new', 'old'])
+ expect(line.type).to be_in(%w(new old))
end
end
end
@@ -251,7 +251,7 @@ FILE
describe '#as_json' do
it 'includes the blob path for the file' do
expect(conflict_file.as_json[:blob_path]).
- to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb")
end
it 'includes the blob icon for the file' do
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index 16eb3766356..2570f95dd21 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -120,43 +120,61 @@ CONFLICT
end
context 'when the file contents include conflict delimiters' do
- it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
- expect { parse_text('=======') }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
- expect { parse_text('>>>>>>> README.md') }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
- expect { parse_text('>>>>>>> some-other-path.md') }.
- not_to raise_error
+ context 'when there is a non-start delimiter first' do
+ it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
+ expect { parse_text('=======') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'raises UnexpectedDelimiter when there is an end delimiter first' do
+ expect { parse_text('>>>>>>> README.md') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'does not raise when there is an end delimiter for a different path first' do
+ expect { parse_text('>>>>>>> some-other-path.md') }.
+ not_to raise_error
+ end
end
- it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
- start_text = "<<<<<<< README.md\n"
- end_text = "\n=======\n>>>>>>> README.md"
+ context 'when a start delimiter is followed by a non-middle delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n" }
+ let(:end_text) { "\n=======\n>>>>>>> README.md" }
- expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + start_text + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
- not_to raise_error
+ it 'does not raise when it is followed by a start delimiter for a different path' do
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
end
- it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
- start_text = "<<<<<<< README.md\n=======\n"
- end_text = "\n>>>>>>> README.md"
+ context 'when a middle delimiter is followed by a non-end delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n=======\n" }
+ let(:end_text) { "\n>>>>>>> README.md" }
- expect { parse_text(start_text + '=======' + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
+ expect { parse_text(start_text + '=======' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + start_text + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
- not_to raise_error
+ it 'does not raise when it is followed by a start delimiter for another path' do
+ expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
end
it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
@@ -184,9 +202,20 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
- it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
- expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
- to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ # All text from Rugged has an encoding of ASCII_8BIT, so force that in
+ # these strings.
+ context 'when the file contains UTF-8 characters' do
+ it 'does not raise' do
+ expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }.
+ not_to raise_error
+ end
+ end
+
+ context 'when the file contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index f01c42aff91..4ce4e6e1034 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
-class MigrationTest
- include Gitlab::Database
-end
-
describe Gitlab::Database, lib: true do
+ before do
+ stub_const('MigrationTest', Class.new { include Gitlab::Database })
+ end
+
+ describe '.config' do
+ it 'returns a Hash' do
+ expect(described_class.config).to be_an_instance_of(Hash)
+ end
+ end
+
describe '.adapter_name' do
it 'returns the name of the adapter' do
expect(described_class.adapter_name).to be_an_instance_of(String)
@@ -119,9 +125,24 @@ describe Gitlab::Database, lib: true do
it 'creates a new connection pool with specific pool size' do
pool = described_class.create_connection_pool(5)
- expect(pool)
- .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
- expect(pool.spec.config[:pool]).to eq(5)
+ begin
+ expect(pool)
+ .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
+
+ expect(pool.spec.config[:pool]).to eq(5)
+ ensure
+ pool.disconnect!
+ end
+ end
+
+ it 'allows setting of a custom hostname' do
+ pool = described_class.create_connection_pool(5, '127.0.0.1')
+
+ begin
+ expect(pool.spec.config[:host]).to eq('127.0.0.1')
+ ensure
+ pool.disconnect!
+ end
end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 5893485634d..c6bd4e81f4f 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks unchanged lines' do
- code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
+ code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
expect(subject[2].text).to eq(code)
end
it 'highlights and marks removed lines' do
- code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
@@ -53,21 +53,21 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'marks unchanged lines' do
- code = %Q{ def popen(cmd, path=nil)}
+ code = %q{ def popen(cmd, path=nil)}
expect(subject[2].text).to eq(code)
expect(subject[2].text).not_to be_html_safe
end
it 'marks removed lines' do
- code = %Q{- raise "System commands must be given as an array of strings"}
+ code = %q{- raise "System commands must be given as an array of strings"}
expect(subject[4].text).to eq(code)
expect(subject[4].text).not_to be_html_safe
end
it 'marks added lines' do
- code = %Q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+ code = %q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
expect(subject[5].text).to eq(code)
expect(subject[5].text).to be_html_safe
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index b983d73f8be..e76128ecd87 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -91,6 +91,54 @@ eos
end
end
+ describe '\ No newline at end of file' do
+ it "parses nonewline in one file correctly" do
+ first_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ +ipsum
+ lorem
+ -ipsum
+ \\ No newline at end of file
+ END
+ lines = parser.parse(first_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('new')
+ expect(lines[0].text).to eq('+ipsum')
+ expect(lines[2].type).to eq('old')
+ expect(lines[3].type).to eq('old-nonewline')
+ expect(lines[1].old_pos).to eq(1)
+ expect(lines[1].new_pos).to eq(2)
+ end
+
+ it "parses nonewline in two files correctly" do
+ both_nonewline_diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ -lorem
+ -ipsum
+ \\ No newline at end of file
+ +ipsum
+ +lorem
+ \\ No newline at end of file
+ END
+ lines = parser.parse(both_nonewline_diff.lines).to_a
+
+ expect(lines[0].type).to eq('old')
+ expect(lines[1].type).to eq('old')
+ expect(lines[2].type).to eq('old-nonewline')
+ expect(lines[5].type).to eq('new-nonewline')
+ expect(lines[3].text).to eq('+ipsum')
+ expect(lines[3].old_pos).to eq(3)
+ expect(lines[3].new_pos).to eq(1)
+ expect(lines[4].text).to eq('+lorem')
+ expect(lines[4].old_pos).to eq(3)
+ expect(lines[4].new_pos).to eq(2)
+ end
+ end
+
context 'when lines is empty' do
it { expect(parser.parse([])).to eq([]) }
it { expect(parser.parse(nil)).to eq([]) }
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
new file mode 100644
index 00000000000..8b5bfc4dbb0
--- /dev/null
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+
+describe Gitlab::EtagCaching::Middleware do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:app_status_code) { 200 }
+ let(:if_none_match) { nil }
+ let(:enabled_path) { '/gitlab-org/gitlab-ce/noteable/issue/1/notes' }
+
+ context 'when ETag caching is not enabled for current route' do
+ let(:path) { '/gitlab-org/gitlab-ce/tree/master/noteable/issue/1/notes' }
+
+ before do
+ mock_app_response
+ end
+
+ it 'does not add ETag header' do
+ _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+ expect(headers['ETag']).to be_nil
+ end
+
+ it 'passes status code from app' do
+ status, _, _ = middleware.call(build_env(path, if_none_match))
+
+ expect(status).to eq app_status_code
+ end
+ end
+
+ context 'when there is no ETag in store for given resource' do
+ let(:path) { enabled_path }
+
+ before do
+ mock_app_response
+ mock_value_in_store(nil)
+ end
+
+ it 'generates ETag' do
+ expect_any_instance_of(Gitlab::EtagCaching::Store)
+ .to receive(:touch).and_return('123')
+
+ middleware.call(build_env(path, if_none_match))
+ end
+
+ context 'when If-None-Match header was specified' do
+ let(:if_none_match) { 'W/"abc"' }
+
+ it 'tracks "etag_caching_key_not_found" event' do
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_middleware_used)
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_key_not_found)
+
+ middleware.call(build_env(path, if_none_match))
+ end
+ end
+ end
+
+ context 'when there is ETag in store for given resource' do
+ let(:path) { enabled_path }
+
+ before do
+ mock_app_response
+ mock_value_in_store('123')
+ end
+
+ it 'returns this value as header' do
+ _, headers, _ = middleware.call(build_env(path, if_none_match))
+
+ expect(headers['ETag']).to eq 'W/"123"'
+ end
+ end
+
+ context 'when If-None-Match header matches ETag in store' do
+ let(:path) { enabled_path }
+ let(:if_none_match) { 'W/"123"' }
+
+ before do
+ mock_value_in_store('123')
+ end
+
+ it 'does not call app' do
+ expect(app).not_to receive(:call)
+
+ middleware.call(build_env(path, if_none_match))
+ end
+
+ it 'returns status code 304' do
+ status, _, _ = middleware.call(build_env(path, if_none_match))
+
+ expect(status).to eq 304
+ end
+
+ it 'tracks "etag_caching_cache_hit" event' do
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_middleware_used)
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_cache_hit)
+
+ middleware.call(build_env(path, if_none_match))
+ end
+ end
+
+ context 'when If-None-Match header does not match ETag in store' do
+ let(:path) { enabled_path }
+ let(:if_none_match) { 'W/"abc"' }
+
+ before do
+ mock_value_in_store('123')
+ end
+
+ it 'calls app' do
+ expect(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+
+ middleware.call(build_env(path, if_none_match))
+ end
+
+ it 'tracks "etag_caching_resource_changed" event' do
+ mock_app_response
+
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_middleware_used)
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_resource_changed)
+
+ middleware.call(build_env(path, if_none_match))
+ end
+ end
+
+ context 'when If-None-Match header is not specified' do
+ let(:path) { enabled_path }
+
+ before do
+ mock_value_in_store('123')
+ mock_app_response
+ end
+
+ it 'tracks "etag_caching_header_missing" event' do
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_middleware_used)
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:etag_caching_header_missing)
+
+ middleware.call(build_env(path, if_none_match))
+ end
+ end
+
+ def mock_app_response
+ allow(app).to receive(:call).and_return([app_status_code, {}, ['body']])
+ end
+
+ def mock_value_in_store(value)
+ allow_any_instance_of(Gitlab::EtagCaching::Store)
+ .to receive(:get).and_return(value)
+ end
+
+ def build_env(path, if_none_match)
+ {
+ 'PATH_INFO' => path,
+ 'HTTP_IF_NONE_MATCH' => if_none_match
+ }
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
index 79b1311ac91..17d6be470ac 100644
--- a/spec/lib/gitlab/git/blob_snippet_spec.rb
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Git::BlobSnippet, seed_helper: true do
end
context 'present lines' do
- let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', %w(wow much), 1, 'wow.rb') }
it { expect(snippet.data).to eq("wow\nmuch") }
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 84f79ec2391..8049e2c120d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :commit do
- let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
-
- let(:commit_options) do
- {
- file: {
- content: 'Lorem ipsum...',
- path: 'documents/story.txt'
- },
- author: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- committer: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- commit: {
- message: 'Wow such commit',
- branch: 'fix-mode'
- }
- }
- end
-
- let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
- let(:commit) { repository.lookup(commit_sha) }
-
- it 'should add file with commit' do
- # Commit message valid
- expect(commit.message).to eq('Wow such commit')
-
- tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
-
- # Directory was created
- expect(tree[:type]).to eq(:tree)
-
- # File was created
- expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
- end
-
- describe "ref updating" do
- it 'creates a commit but does not udate a ref' do
- commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
- commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
- commit = repository.lookup(commit_sha)
-
- # Commit message valid
- expect(commit.message).to eq('Wow such commit')
-
- # Does not update any related ref
- expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
- expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
- end
- end
-
- describe 'reject updates' do
- it 'should reject updates' do
- commit_options[:file][:update] = false
- commit_options[:file][:path] = 'files/executables/ls'
-
- expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
- end
- end
-
- describe 'file modes' do
- it 'should preserve file modes with commit' do
- commit_options[:file][:path] = 'files/executables/ls'
-
- entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
- expect(entry[:filemode]).to eq(0100755)
- end
- end
- end
-
- describe :rename do
- let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
- let(:rename_options) do
- {
- file: {
- path: 'NEWCONTRIBUTING.md',
- previous_path: 'CONTRIBUTING.md',
- content: 'Lorem ipsum...',
- update: true
- },
- author: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- committer: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- commit: {
- message: 'Rename readme',
- branch: 'master'
- }
- }
- end
-
- let(:rename_options2) do
- {
- file: {
- content: 'Lorem ipsum...',
- path: 'bin/new_executable',
- previous_path: 'bin/executable',
- },
- author: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- committer: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- commit: {
- message: 'Updates toberenamed.txt',
- branch: 'master',
- update_ref: false
- }
- }
- end
-
- it 'maintains file permissions when renaming' do
- mode = 0o100755
-
- Gitlab::Git::Blob.rename(repository, rename_options2)
-
- expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
- end
-
- it 'renames the file with commit and not change file permissions' do
- ref = rename_options[:commit][:branch]
-
- expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
- expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
-
- expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
- expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
- end
- end
-
- describe :remove do
- let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
-
- let(:commit_options) do
- {
- file: {
- path: 'README.md'
- },
- author: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- committer: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- commit: {
- message: 'Remove readme',
- branch: 'feature'
- }
- }
- end
-
- let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
- let(:commit) { repository.lookup(commit_sha) }
- let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
-
- it 'should remove file with commit' do
- # Commit message valid
- expect(commit.message).to eq('Remove readme')
-
- # File was removed
- expect(blob).to be_nil
- end
- end
-
describe :lfs_pointers do
context 'file a valid lfs pointer' do
let(:blob) do
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 4fa72c565ae..47bdd7310d5 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -365,7 +365,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when go over safe limits on files' do
- let(:iterator) { [ fake_diff(1, 1) ] * 4 }
+ let(:iterator) { [fake_diff(1, 1)] * 4 }
before(:each) do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4c55532d165..992126ef153 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -109,6 +109,43 @@ EOT
end
end
end
+
+ context 'using a Gitaly::CommitDiffResponse' do
+ let(:diff) do
+ described_class.new(
+ Gitaly::CommitDiffResponse.new(
+ to_path: ".gitmodules",
+ from_path: ".gitmodules",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ raw_chunks: raw_chunks,
+ )
+ )
+ end
+
+ context 'with a small diff' do
+ let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash)
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ let(:raw_chunks) { ['a' * 204800] }
+
+ it 'prunes the diff' do
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+ end
end
describe 'straight diffs' do
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
new file mode 100644
index 00000000000..d0c7ca60ddc
--- /dev/null
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -0,0 +1,220 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Index, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:index) { described_class.new(repository) }
+
+ before do
+ index.read_tree(repository.lookup('master').tree)
+ end
+
+ describe '#create' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ file_path: 'documents/story.txt'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ it 'creates the file in the index' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).not_to be_nil
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+ end
+
+ context 'when a file at that path exists' do
+ before do
+ options[:file_path] = 'files/executables/ls'
+ end
+
+ it 'raises an error' do
+ expect { index.create(options) }.to raise_error('Filename already exists')
+ end
+ end
+
+ context 'when content is in base64' do
+ before do
+ options[:content] = Base64.encode64(options[:content])
+ options[:encoding] = 'base64'
+ end
+
+ it 'decodes base64' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+ expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content]))
+ end
+ end
+
+ context 'when content contains CRLF' do
+ before do
+ repository.autocrlf = :input
+ options[:content] = "Hello,\r\nWorld"
+ end
+
+ it 'converts to LF' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+ expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld")
+ end
+ end
+ end
+
+ describe '#create_dir' do
+ let(:options) do
+ {
+ file_path: 'newdir'
+ }
+ end
+
+ context 'when no file or dir at that path exists' do
+ it 'creates the dir in the index' do
+ index.create_dir(options)
+
+ entry = index.get(options[:file_path] + '/.gitkeep')
+
+ expect(entry).not_to be_nil
+ end
+ end
+
+ context 'when a file at that path exists' do
+ before do
+ options[:file_path] = 'files/executables/ls'
+ end
+
+ it 'raises an error' do
+ expect { index.create_dir(options) }.to raise_error('Directory already exists as a file')
+ end
+ end
+
+ context 'when a directory at that path exists' do
+ before do
+ options[:file_path] = 'files/executables'
+ end
+
+ it 'raises an error' do
+ expect { index.create_dir(options) }.to raise_error('Directory already exists')
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ file_path: 'README.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ before do
+ options[:file_path] = 'documents/story.txt'
+ end
+
+ it 'raises an error' do
+ expect { index.update(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'updates the file in the index' do
+ index.update(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+
+ it 'preserves file mode' do
+ options[:file_path] = 'files/executables/ls'
+
+ index.update(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry[:mode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe '#move' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ previous_path: 'README.md',
+ file_path: 'NEWREADME.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ it 'raises an error' do
+ options[:previous_path] = 'documents/story.txt'
+
+ expect { index.move(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'removes the old file in the index' do
+ index.move(options)
+
+ entry = index.get(options[:previous_path])
+
+ expect(entry).to be_nil
+ end
+
+ it 'creates the new file in the index' do
+ index.move(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).not_to be_nil
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+
+ it 'preserves file mode' do
+ options[:previous_path] = 'files/executables/ls'
+
+ index.move(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry[:mode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe '#delete' do
+ let(:options) do
+ {
+ file_path: 'README.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ before do
+ options[:file_path] = 'documents/story.txt'
+ end
+
+ it 'raises an error' do
+ expect { index.delete(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'removes the file in the index' do
+ index.delete(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 2a915bf426f..9ed43da1116 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -47,7 +47,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe :branch_names do
+ describe '#branch_names' do
subject { repository.branch_names }
it 'has SeedRepo::Repo::BRANCHES.size elements' do
@@ -57,7 +57,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.not_to include("branch-from-space") }
end
- describe :tag_names do
+ describe '#tag_names' do
subject { repository.tag_names }
it { is_expected.to be_kind_of Array }
@@ -78,49 +78,69 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { expect(metadata['ArchivePath']).to end_with extenstion }
end
- describe :archive do
+ describe '#archive_prefix' do
+ let(:project_name) { 'project-name'}
+
+ before do
+ expect(repository).to receive(:name).once.and_return(project_name)
+ end
+
+ it 'returns parameterised string for a ref containing slashes' do
+ prefix = repository.archive_prefix('test/branch', 'SHA')
+
+ expect(prefix).to eq("#{project_name}-test-branch-SHA")
+ end
+
+ it 'returns correct string for a ref containing dots' do
+ prefix = repository.archive_prefix('test.branch', 'SHA')
+
+ expect(prefix).to eq("#{project_name}-test.branch-SHA")
+ end
+ end
+
+ describe '#archive' do
let(:metadata) { repository.archive_metadata('master', '/tmp') }
it_should_behave_like 'archive check', '.tar.gz'
end
- describe :archive_zip do
+ describe '#archive_zip' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') }
it_should_behave_like 'archive check', '.zip'
end
- describe :archive_bz2 do
+ describe '#archive_bz2' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') }
it_should_behave_like 'archive check', '.tar.bz2'
end
- describe :archive_fallback do
+ describe '#archive_fallback' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') }
it_should_behave_like 'archive check', '.tar.gz'
end
- describe :size do
+ describe '#size' do
subject { repository.size }
it { is_expected.to be < 2 }
end
- describe :has_commits? do
+ describe '#has_commits?' do
it { expect(repository.has_commits?).to be_truthy }
end
- describe :empty? do
+ describe '#empty?' do
it { expect(repository.empty?).to be_falsey }
end
- describe :bare? do
+ describe '#bare?' do
it { expect(repository.bare?).to be_truthy }
end
- describe :heads do
+ describe '#heads' do
let(:heads) { repository.heads }
subject { heads }
@@ -147,7 +167,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe :ref_names do
+ describe '#ref_names' do
let(:ref_names) { repository.ref_names }
subject { ref_names }
@@ -164,7 +184,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe :search_files do
+ describe '#search_files' do
let(:results) { repository.search_files('rails', 'master') }
subject { results }
@@ -200,7 +220,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context :submodules do
+ context '#submodules' do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
context 'where repo has submodules' do
@@ -264,7 +284,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe :commit_count do
+ describe '#commit_count' do
it { expect(repository.commit_count("master")).to eq(25) }
it { expect(repository.commit_count("feature")).to eq(9) }
end
@@ -493,7 +513,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#remote_add" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
- @repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
+ @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
end
it "should add the remote" do
@@ -529,7 +549,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = nil
rename_commit = nil
- before(:all) do
+ before(:context) do
# Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
@@ -538,49 +558,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = new_commit_edit_new_file(repo)
end
+ after(:context) do
+ # Erase our commits so other tests get the original repo
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+ end
+
context "where 'follow' == true" do
- options = { ref: "master", follow: true }
+ let(:options) { { ref: "master", follow: true } }
context "and 'path' is a directory" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding"))
- end
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "encoding"))
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_old_name)
+ aggregate_failures do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
end
end
context "and 'path' is a file that matches the new filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding/CHANGELOG"))
+ context 'without offset' do
+ it "follows renames" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
+
+ aggregate_failures do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
end
- it "should follow renames" do
- expect(log_commits).to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).to include(commit_with_old_name)
+ context 'with offset=1' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=1', 'and limit=1' do
+ it "follows renames, skip the latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
+
+ expect(log_commits).to contain_exactly(rename_commit)
+ end
+ end
+
+ context 'with offset=1', 'and limit=2' do
+ it "follows renames, skip the latest commit and return only two commits" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
+
+ aggregate_failures do
+ expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=2' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=2', 'and limit=1' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
+
+ expect(log_commits).to contain_exactly(commit_with_old_name)
+ end
+ end
+
+ context 'with offset=2', 'and limit=2' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
end
end
context "and 'path' is a file that matches the old filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "CHANGELOG"))
- end
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "CHANGELOG"))
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_old_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_new_name)
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
end
end
context "unknown ref" do
- let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
+ it "returns an empty array" do
+ log_commits = repository.log(options.merge(ref: 'unknown'))
- it "should return empty" do
expect(log_commits).to eq([])
end
end
@@ -699,12 +789,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
-
- after(:all) do
- # Erase our commits so other tests get the original repo
- repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
- repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
- end
end
describe "#commits_between" do
@@ -746,6 +830,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to eq(17) }
end
+ describe '#count_commits' do
+ context 'with after timestamp' do
+ it 'returns the number of commits after timestamp' do
+ options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') }
+
+ expect(repository.count_commits(options)).to eq(25)
+ end
+ end
+
+ context 'with before timestamp' do
+ it 'returns the number of commits after timestamp' do
+ options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') }
+
+ expect(repository.count_commits(options)).to eq(9)
+ end
+ end
+
+ context 'with path' do
+ it 'returns the number of commits with path ' do
+ options = { ref: 'master', limit: nil, path: "encoding" }
+
+ expect(repository.count_commits(options)).to eq(2)
+ end
+ end
+ end
+
describe "branch_names_contains" do
subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
@@ -844,81 +954,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#mkdir' do
- let(:commit_options) do
- {
- author: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- committer: {
- email: 'user@example.com',
- name: 'Test User',
- time: Time.now
- },
- commit: {
- message: 'Test message',
- branch: 'refs/heads/fix',
- }
- }
- end
-
- def generate_diff_for_path(path)
- "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
-new file mode 100644
-index 0000000..e69de29
---- /dev/null
-+++ b/#{path}/.gitkeep\n"
- end
-
- shared_examples 'mkdir diff check' do |path, expected_path|
- it 'creates a directory' do
- result = repository.mkdir(path, commit_options)
- expect(result).not_to eq(nil)
-
- # Verify another mkdir doesn't create a directory that already exists
- expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
- end
- end
-
- describe 'creates a directory in root directory' do
- it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
- end
-
- describe 'creates a directory in subdirectory' do
- it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
- end
-
- describe 'creates a directory in subdirectory with a slash' do
- it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
- end
-
- describe 'creates a directory in subdirectory with multiple slashes' do
- it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
- end
-
- describe 'handles relative paths' do
- it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
- end
-
- describe 'creates nested directories' do
- it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
- end
-
- it 'does not attempt to create a directory with invalid relative path' do
- expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
- end
-
- it 'does not attempt to overwrite a file' do
- expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
- end
-
- it 'does not attempt to overwrite a directory' do
- expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
- end
- end
-
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a55bd4387e0..48f7754bed8 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -199,7 +199,9 @@ describe Gitlab::GitAccess, lib: true do
def stub_git_hooks
# Running the `pre-receive` hook is expensive, and not necessary for this test.
- allow_any_instance_of(GitHooksService).to receive(:execute).and_yield
+ allow_any_instance_of(GitHooksService).to receive(:execute) do |service, &block|
+ block.call(service)
+ end
end
def merge_into_protected_branch
@@ -207,13 +209,12 @@ describe Gitlab::GitAccess, lib: true do
stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
target_branch = project.repository.lookup('feature')
- source_branch = project.repository.commit_file(
+ source_branch = project.repository.create_file(
user,
FFaker::InternetSE.login_user_name,
FFaker::HipsterIpsum.paragraph,
message: FFaker::HipsterIpsum.sentence,
- branch_name: unprotected_branch,
- update: false)
+ branch_name: unprotected_branch)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
@@ -232,11 +233,18 @@ describe Gitlab::GitAccess, lib: true do
else
project.team << [user, role]
end
+ end
+
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.send(:check_push_access!, changes[action]) }
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.send(:check_push_access!, changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ it do
+ if allowed
+ expect { subject }.not_to raise_error
+ else
+ expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
+ end
end
end
end
@@ -301,7 +309,7 @@ describe Gitlab::GitAccess, lib: true do
}
}
- [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
context do
before { create(:protected_branch, name: protected_branch_name, project: project) }
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 219198eff60..8eaf7aac264 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Git, lib: true do
describe 'committer_hash' do
it "returns a hash containing the given email and name" do
- committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+ committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: committer_name)
expect(committer_hash[:email]).to eq(committer_email)
expect(committer_hash[:name]).to eq(committer_name)
@@ -28,7 +28,7 @@ describe Gitlab::Git, lib: true do
context 'when email is nil' do
it "returns nil" do
- committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+ committer_hash = Gitlab::Git.committer_hash(email: nil, name: committer_name)
expect(committer_hash).to be_nil
end
@@ -36,7 +36,7 @@ describe Gitlab::Git, lib: true do
context 'when name is nil' do
it "returns nil" do
- committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+ committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: nil)
expect(committer_hash).to be_nil
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
new file mode 100644
index 00000000000..4684b1d1ac0
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Commit do
+ describe '.diff_from_parent' do
+ let(:diff_stub) { double('Gitaly::Diff::Stub') }
+ let(:project) { create(:project, :repository) }
+ let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
+ let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+
+ before do
+ allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
+ allow(diff_stub).to receive(:commit_diff).and_return([])
+ end
+
+ context 'when a commit has a parent' do
+ it 'sends an RPC request with the parent ID as left commit' do
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+ right_commit_id: commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(commit)
+ end
+ end
+
+ context 'when a commit does not have a parent' do
+ it 'sends an RPC request with empty tree ref as left commit' do
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ right_commit_id: initial_commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(initial_commit)
+ end
+ end
+
+ it 'returns a Gitlab::Git::DiffCollection' do
+ ret = described_class.diff_from_parent(commit)
+
+ expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
+ end
+
+ it 'passes options to Gitlab::Git::DiffCollection' do
+ options = { max_files: 31, max_lines: 13 }
+
+ expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+
+ described_class.diff_from_parent(commit, options)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
new file mode 100644
index 00000000000..a6252c99aa1
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Notifications do
+ let(:client) { Gitlab::GitalyClient::Notifications.new }
+
+ before do
+ allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
+ end
+
+ describe '#post_receive' do
+ let(:repo_path) { '/path/to/my_repo.git' }
+
+ it 'sends a post_receive message' do
+ expect_any_instance_of(Gitaly::Notifications::Stub).
+ to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path))
+
+ client.post_receive(repo_path)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index 36e7d739f7e..3a31f93efa5 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -6,27 +6,27 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
let(:repo) { double }
let(:raw) do
{
- ref: 'feature',
+ ref: 'branch-merged',
repo: repo,
sha: commit.id
}
end
describe '#exists?' do
- it 'returns true when both branch, and commit exists' do
+ it 'returns true when branch exists and commit is part of the branch' do
branch = described_class.new(project, double(raw))
expect(branch.exists?).to eq true
end
- it 'returns false when branch does not exist' do
- branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
+ it 'returns false when branch exists and commit is not part of the branch' do
+ branch = described_class.new(project, double(raw.merge(ref: 'feature')))
expect(branch.exists?).to eq false
end
- it 'returns false when commit does not exist' do
- branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
+ it 'returns false when branch does not exist' do
+ branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
expect(branch.exists?).to eq false
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 33d83d6d2f1..8b867fbe322 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:label1) do
double(
name: 'Bug',
@@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
- let!(:user) { create(:user, email: octocat.email) }
- let(:repository) { double(id: 1, fork: false) }
- let(:source_sha) { create(:commit, project: project).id }
- let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
- let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
- let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
- let(:pull_request) do
- double(
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'New feature',
- body: 'Please pull these awesome changes',
- head: source_branch,
- base: target_branch,
- assignee: nil,
- user: octocat,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- merged_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
- labels: [double(name: 'Label #2')]
- )
- end
-
let(:release1) do
double(
tag_name: 'v1.0.0',
@@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
+ subject { described_class.new(project) }
+
it 'returns true' do
- expect(described_class.new(project).execute).to eq true
+ expect(subject.execute).to eq true
end
it 'does not raise an error' do
- expect { described_class.new(project).execute }.not_to raise_error
+ expect { subject.execute }.not_to raise_error
end
it 'stores error messages' do
@@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do
end
end
+ shared_examples 'Gitlab::GithubImport unit-testing' do
+ describe '#clean_up_restored_branches' do
+ subject { described_class.new(project) }
+
+ before do
+ allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
+ allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
+ end
+
+ context 'when pull request stills open' do
+ let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) }
+
+ it 'does not remove branches' do
+ expect(subject).not_to receive(:remove_branch)
+ subject.send(:clean_up_restored_branches, gh_pull_request)
+ end
+ end
+
+ context 'when pull request is closed' do
+ let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) }
+
+ it 'does remove branches' do
+ expect(subject).to receive(:remove_branch).at_least(2).times
+ subject.send(:clean_up_restored_branches, gh_pull_request)
+ end
+ end
+ end
+ end
+
let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:credentials) { { user: 'joe' } }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+ let(:closed_pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'closed',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: updated_at,
+ merged_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+
context 'when importing a GitHub project' do
let(:api_root) { 'https://api.github.com' }
let(:repo_root) { 'https://github.com' }
+ subject { described_class.new(project) }
it_behaves_like 'Gitlab::GithubImport::Importer#execute'
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::GithubImport unit-testing'
describe '#client' do
it 'instantiates a Client' do
@@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{}
)
- described_class.new(project).client
+ subject.client
end
end
end
@@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
context 'when importing a Gitea project' do
let(:api_root) { 'https://try.gitea.io/api/v1' }
let(:repo_root) { 'https://try.gitea.io' }
+ subject { described_class.new(project) }
+
before do
project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
end
@@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
let(:expected_not_called) { [:import_releases] }
end
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::GithubImport unit-testing'
describe '#client' do
it 'instantiates a Client' do
@@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{ host: "#{repo_root}:443/foo", api_version: 'v1' }
)
- described_class.new(project).client
+ subject.client
end
end
end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index e46be18aa99..44423917944 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -7,10 +7,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:repository) { double(id: 1, fork: false) }
let(:source_repo) { repository }
- let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
+ let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
+ let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
+ let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -49,7 +51,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -75,7 +77,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -102,7 +104,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -194,7 +196,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:raw_data) { double(base_data) }
it 'returns branch ref' do
- expect(pull_request.source_branch_name).to eq 'feature'
+ expect(pull_request.source_branch_name).to eq 'branch-merged'
end
end
@@ -205,10 +207,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
end
end
+
+ context 'when source branch is from a fork' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'prefixes branch name with pull request number and project with namespace to avoid collision' do
+ expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master'
+ end
+ end
end
shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
- context 'when source branch exists' do
+ context 'when target branch exists' do
let(:raw_data) { double(base_data) }
it 'returns branch ref' do
@@ -271,6 +281,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ describe '#cross_project?' do
+ context 'when source and target repositories are different' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+
+ context 'when source and target repositories are the same' do
+ let(:raw_data) { double(base_data.merge(head: source_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.cross_project?).to eq false
+ end
+ end
+ end
+
describe '#url' do
let(:raw_data) { double(base_data) }
@@ -278,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
end
end
+
+ describe '#opened?' do
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+ it 'returns true when state is "open"' do
+ expect(pull_request.opened?).to be_truthy
+ end
+ end
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e177d883158..e49799ad105 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
end
it 'highlights all the lines properly' do
- expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
- expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
- expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
+ expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
+ expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
+ expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end
describe 'with CRLF' do
@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
end
it 'strips extra LFs' do
- expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>")
+ expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9c08f41fe82..c3ee743035a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -97,6 +97,7 @@ variables:
triggers:
- project
- trigger_requests
+- owner
deploy_keys:
- user
- deploy_keys_projects
@@ -131,12 +132,12 @@ project:
- campfire_service
- drone_ci_service
- emails_on_push_service
-- builds_email_service
- pipelines_email_service
- mattermost_slash_commands_service
- slack_slash_commands_service
- irker_service
- pivotaltracker_service
+- prometheus_service
- hipchat_service
- flowdock_service
- assembla_service
@@ -155,6 +156,7 @@ project:
- gitlab_issue_tracker_service
- external_wiki_service
- kubernetes_service
+- mock_ci_service
- forked_project_link
- forked_from_project
- forked_project_links
@@ -200,6 +202,7 @@ project:
- route
- statistics
- container_images
+- uploads
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index ea65a5dfed1..e24d070706a 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -17,7 +17,7 @@ describe 'Import/Export attribute configuration', lib: true do
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
- names.flatten.uniq - ['milestones', 'labels'] + ['project']
+ names.flatten.uniq - %w(milestones labels) + ['project']
end
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
index d6ee94442cb..579a31ead58 100644
--- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::AvatarSaver, lib: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:project) { create(:empty_project) }
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
index a88ddd17aca..b88b9c18c15 100644
--- a/spec/lib/gitlab/import_export/file_importer_spec.rb
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::FileImporter, lib: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
- let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" }
let(:valid_file) { "#{shared.export_path}/valid.json" }
let(:symlink_file) { "#{shared.export_path}/invalid.json" }
let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
index 20743811dab..f3fd0d82875 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::ImportExport, services: true do
end
it 'contains the namespace path' do
- expect(described_class.export_filename(project: project)).to include(project.namespace.full_path)
+ expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_'))
end
it 'does not go over a certain length' do
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 9b492d1b9c7..2ede5cdd2ad 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -14,7 +14,7 @@ describe 'Import/Export model configuration', lib: true do
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
# - User, Author... Models we do not care about for checking models
- names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+ names.flatten.uniq - %w(milestones labels user author) + ['project']
end
let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 2e9f60432b4..d9b67426818 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2539,7 +2539,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": true,
+ "merge_when_pipeline_succeeds": true,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -2976,7 +2976,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -3260,7 +3260,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -3544,7 +3544,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -4234,7 +4234,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -4782,7 +4782,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -5281,7 +5281,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -5541,7 +5541,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -6231,7 +6231,7 @@
"merge_params": {
"force_remove_source_branch": null
},
- "merge_when_build_succeeds": false,
+ "merge_when_pipeline_succeeds": false,
"merge_user_id": null,
"merge_commit_sha": null,
"deleted_at": null,
@@ -6507,7 +6507,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6565,7 +6564,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6603,7 +6601,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
},
@@ -6624,7 +6621,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6659,7 +6655,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
},
@@ -6695,7 +6690,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6716,7 +6710,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6751,7 +6744,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
},
@@ -6787,7 +6779,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
},
@@ -6808,7 +6799,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6843,7 +6833,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
},
@@ -6879,7 +6868,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
},
@@ -6900,7 +6888,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6935,7 +6922,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6971,7 +6957,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
},
@@ -6985,11 +6970,10 @@
{
"id": 123,
"token": "cdbfasdf44a5958c83654733449e585",
- "project_id": null,
+ "project_id": 5,
"deleted_at": null,
"created_at": "2017-01-16T15:25:28.637Z",
- "updated_at": "2017-01-16T15:25:28.637Z",
- "gl_project_id": 123
+ "updated_at": "2017-01-16T15:25:28.637Z"
}
],
"deploy_keys": [
@@ -7047,7 +7031,7 @@
"updated_at": "2016-06-14T15:01:51.303Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7055,7 +7039,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7174,7 +7158,7 @@
"updated_at": "2016-06-14T15:01:51.219Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7182,7 +7166,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,27 +7319,6 @@
"wiki_page_events": true
},
{
- "id": 85,
- "title": "Builds emails",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.090Z",
- "updated_at": "2016-06-14T15:01:51.090Z",
- "active": false,
- "properties": {
- "notify_only_broken_builds": true
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
"id": 84,
"title": "Buildkite",
"project_id": 5,
@@ -7503,4 +7466,4 @@
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
-} \ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index f4a21c24fa1..c36f12dbd82 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Ci::Build.where(token: 'abcd')).to be_empty
end
end
+
+ context 'has restored the correct number of records' do
+ it 'has the correct number of merge requests' do
+ expect(@project.merge_requests.size).to eq(9)
+ end
+
+ it 'has the correct number of triggers' do
+ expect(@project.triggers.size).to eq(1)
+ end
+
+ it 'has the correct number of pipelines and statuses' do
+ expect(@project.pipelines.size).to eq(5)
+
+ @project.pipelines.zip([2, 2, 2, 2, 2])
+ .each do |(pipeline, expected_status_size)|
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 3628adefc0c..012c22ec5ad 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
describe 'saves the project tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
@@ -114,7 +114,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
it 'has project and group labels' do
label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
+ expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
index d39ea60ff7f..a7f4e11271e 100644
--- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::RepoSaver, services: true do
describe 'bundle a project Git repo' do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:bundler) { described_class.new(project: project, shared: shared) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index c5ac702d831..1ad16a9b57d 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,12 +15,14 @@ Issue:
- updated_by_id
- confidential
- deleted_at
+- closed_at
- due_date
- moved_to_id
- lock_version
- milestone_id
- weight
- time_estimate
+- relative_position
Event:
- id
- target_type
@@ -142,7 +144,7 @@ MergeRequest:
- updated_by_id
- merge_error
- merge_params
-- merge_when_build_succeeds
+- merge_when_pipeline_succeeds
- merge_user_id
- merge_commit_sha
- deleted_at
@@ -175,7 +177,6 @@ Ci::Pipeline:
- tag
- yaml_errors
- committed_at
-- gl_project_id
- status
- started_at
- finished_at
@@ -210,7 +211,6 @@ CommitStatus:
- target_url
- description
- artifacts_file
-- gl_project_id
- artifacts_metadata
- erased_by_id
- erased_at
@@ -231,7 +231,6 @@ Ci::Variable:
- encrypted_value
- encrypted_value_salt
- encrypted_value_iv
-- gl_project_id
Ci::Trigger:
- id
- token
@@ -239,7 +238,8 @@ Ci::Trigger:
- deleted_at
- created_at
- updated_at
-- gl_project_id
+- owner_id
+- description
DeployKey:
- id
- user_id
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
index 47d5d2fc150..071e5fac3f0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::WikiRepoSaver, services: true do
describe 'bundle a wiki Git repo' do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
let!(:project_wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 8cea38e9ff8..b3b5e5e7e33 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -22,16 +22,16 @@ describe Gitlab::ImportSources do
describe '.values' do
it 'returns an array' do
expected =
- [
- 'github',
- 'bitbucket',
- 'gitlab',
- 'google_code',
- 'fogbugz',
- 'git',
- 'gitlab_project',
- 'gitea'
- ]
+ %w(
+ github
+ bitbucket
+ gitlab
+ google_code
+ fogbugz
+ git
+ gitlab_project
+ gitea
+ )
expect(described_class.values).to eq(expected)
end
@@ -40,15 +40,15 @@ describe Gitlab::ImportSources do
describe '.importer_names' do
it 'returns an array of importer names' do
expected =
- [
- 'github',
- 'bitbucket',
- 'gitlab',
- 'google_code',
- 'fogbugz',
- 'gitlab_project',
- 'gitea'
- ]
+ %w(
+ github
+ bitbucket
+ gitlab
+ google_code
+ fogbugz
+ gitlab_project
+ gitea
+ )
expect(described_class.importer_names).to eq(expected)
end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index c9bd52a3b8f..91f9d06b85a 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Kubernetes do
let(:pod_name) { 'pod1' }
let(:container_name) { 'container1' }
- subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
+ subject(:result) { URI.parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
it { expect(result.scheme).to eq('wss') }
it { expect(result.host).to eq('example.com') }
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 69c49051156..7a2f774b948 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::LDAP::AuthHash, lib: true do
context "with overridden attributes" do
let(:attributes) do
{
- 'username' => ['mail', 'email'],
+ 'username' => %w(mail email),
'name' => 'fullName'
}
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 89790c9e1af..2f3bd4393b7 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -95,10 +95,10 @@ describe Gitlab::LDAP::User, lib: true do
it 'maintains an identity per provider' do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
- expect(existing_user.identities.count).to eql(1)
+ expect(existing_user.identities.count).to be(1)
ldap_user.save
- expect(ldap_user.gl_user.identities.count).to eql(2)
+ expect(ldap_user.gl_user.identities.count).to be(2)
# Expect that find_by provider only returns a single instance of an identity and not an Enumerable
expect(ldap_user.gl_user.identities.find_by(provider: 'twitter')).to be_instance_of Identity
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index d88bcae41fb..a986cb520fb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -197,11 +197,13 @@ describe Gitlab::Metrics::Instrumentation do
@child1 = Class.new(@dummy) do
def self.child1_foo; end
+
def child1_bar; end
end
@child2 = Class.new(@child1) do
def self.child2_foo; end
+
def child2_bar; end
end
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 8d05081eecb..a247f03b2da 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Metrics::MethodCall do
expect(metric.values[:duration]).to be_a_kind_of(Numeric)
expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
- expect(metric.values[:call_count]).to an_instance_of(Fixnum)
+ expect(metric.values[:call_count]).to be_an(Integer)
expect(metric.tags).to eq({ method: 'Foo#bar' })
end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f26fca52c50..d240b8a01fd 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Metrics::Metric do
end
it 'includes the timestamp' do
- expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
+ expect(hash[:timestamp]).to be_an(Integer)
end
end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 9e2ea89a712..4d94d8705fb 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -29,19 +29,19 @@ describe Gitlab::Metrics::System do
describe '.cpu_time' do
it 'returns a Fixnum' do
- expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
+ expect(described_class.cpu_time).to be_an(Integer)
end
end
describe '.real_time' do
it 'returns a Fixnum' do
- expect(described_class.real_time).to be_an_instance_of(Fixnum)
+ expect(described_class.real_time).to be_an(Integer)
end
end
describe '.monotonic_time' do
it 'returns a Fixnum' do
- expect(described_class.monotonic_time).to be_an_instance_of(Fixnum)
+ expect(described_class.monotonic_time).to be_an(Integer)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 3887c04c832..0c5a6246d85 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -134,7 +134,7 @@ describe Gitlab::Metrics::Transaction do
series: 'rails_transactions',
tags: { action: 'Foo#bar' },
values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
- timestamp: an_instance_of(Fixnum)
+ timestamp: a_kind_of(Integer)
}
expect(Gitlab::Metrics).to receive(:submit_metrics).
@@ -151,7 +151,7 @@ describe Gitlab::Metrics::Transaction do
series: 'events',
tags: { event: :meow },
values: { count: 1 },
- timestamp: an_instance_of(Fixnum)
+ timestamp: a_kind_of(Integer)
}
expect(Gitlab::Metrics).to receive(:submit_metrics).
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index fd3769d75b5..c2ab015d5cb 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -15,16 +15,93 @@ describe Gitlab::Middleware::Go, lib: true do
end
describe 'when go-get=1' do
- it 'returns a document' do
- env = { 'rack.input' => '',
- 'QUERY_STRING' => 'go-get=1',
- 'PATH_INFO' => '/group/project/path' }
- resp = middleware.call(env)
- expect(resp[0]).to eq(200)
- expect(resp[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n"
- expect(resp[2].body).to eq([expected_body])
+ let(:current_user) { nil }
+
+ context 'with simple 2-segment project path' do
+ let!(:project) { create(:project, :private) }
+
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+ end
+
+ context 'with a nested project path' do
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :public, namespace: group) }
+
+ shared_examples 'a nested project' do
+ context 'when the project is public' do
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'when the project is private' do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context 'with access to the project' do
+ let(:current_user) { project.creator }
+
+ before do
+ project.team.add_master(current_user)
+ end
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'without access to the project' do
+ it 'returns the 2-segment group path' do
+ expect_response_with_path(go, group.full_path)
+ end
+ end
+ end
+ end
+
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it_behaves_like 'a nested project'
+ end
end
end
+
+ def go
+ env = {
+ 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=1',
+ 'PATH_INFO' => "/#{path}",
+ 'warden' => double(authenticate: current_user)
+ }
+ middleware.call(env)
+ end
+
+ def expect_response_with_path(response, path)
+ expect(response[0]).to eq(200)
+ expect(response[1]['Content-Type']).to eq('text/html')
+ expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n"
+ expect(response[2].body).to eq([expected_body])
+ end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index fc9e1cb430a..6c84a4c8b73 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -148,12 +148,14 @@ describe Gitlab::OAuth::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'johndoe@example.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
- [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
{ provider: 'twitter', extern_uid: uid }
- ])
+ ]
+ )
end
end
@@ -167,12 +169,14 @@ describe Gitlab::OAuth::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.email).to eql 'john@example.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
- [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
{ provider: 'twitter', extern_uid: uid }
- ])
+ ]
+ )
end
end
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
index 498dc514c8c..acce2be93f2 100644
--- a/spec/lib/gitlab/optimistic_locking_spec.rb
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::OptimisticLocking, lib: true do
- describe '#retry_lock' do
- let!(:pipeline) { create(:ci_pipeline) }
- let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+ let!(:pipeline) { create(:ci_pipeline) }
+ let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+ describe '#retry_lock' do
it 'does not reload object if state changes' do
expect(pipeline).not_to receive(:reload)
expect(pipeline).to receive(:succeed).and_call_original
@@ -36,4 +36,17 @@ describe Gitlab::OptimisticLocking, lib: true do
end.to raise_error(ActiveRecord::StaleObjectError)
end
end
+
+ describe '#retry_optimistic_lock' do
+ context 'when locking module is mixed in' do
+ let(:unlockable) do
+ Class.new.include(described_class).new
+ end
+
+ it 'is an alias for retry_lock' do
+ expect(unlockable.method(:retry_optimistic_lock))
+ .to eq unlockable.method(:retry_lock)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb
new file mode 100644
index 00000000000..280264188e2
--- /dev/null
+++ b/spec/lib/gitlab/prometheus_spec.rb
@@ -0,0 +1,143 @@
+require 'spec_helper'
+
+describe Gitlab::Prometheus, lib: true do
+ include PrometheusHelpers
+
+ subject { described_class.new(api_url: 'https://prometheus.example.com') }
+
+ describe '#ping' do
+ it 'issues a "query" request to the API endpoint' do
+ req_stub = stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector'))
+
+ expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] })
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ # This shared examples expect:
+ # - query_url: A query URL
+ # - execute_query: A query call
+ shared_examples 'failure response' do
+ context 'when request returns 400 with an error message' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' })
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, 'bar!')
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns 400 without an error message' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 400)
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, 'Bad data received')
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns 500' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' })
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}')
+ expect(req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#query' do
+ let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+ let(:query_url) { prometheus_query_url(prometheus_query) }
+
+ context 'when request returns vector results' do
+ it 'returns data from the API call' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+ expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns matrix results' do
+ it 'returns nil' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('matrix'))
+
+ expect(subject.query(prometheus_query)).to be_nil
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns no data' do
+ it 'returns []' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('vector'))
+
+ expect(subject.query(prometheus_query)).to be_empty
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ it_behaves_like 'failure response' do
+ let(:execute_query) { subject.query(prometheus_query) }
+ end
+ end
+
+ describe '#query_range' do
+ let(:prometheus_query) { prometheus_memory_query('env-slug') }
+ let(:query_url) { prometheus_query_range_url(prometheus_query) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'when a start time is passed' do
+ let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) }
+
+ it 'passed it in the requested URL' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+ subject.query_range(prometheus_query, start: 2.hours.ago)
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns vector results' do
+ it 'returns nil' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+ expect(subject.query_range(prometheus_query)).to be_nil
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns matrix results' do
+ it 'returns data from the API call' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('matrix'))
+
+ expect(subject.query_range(prometheus_query)).to eq([
+ {
+ "metric" => {},
+ "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]]
+ }
+ ])
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns no data' do
+ it 'returns []' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('matrix'))
+
+ expect(subject.query_range(prometheus_query)).to be_empty
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ it_behaves_like 'failure response' do
+ let(:execute_query) { subject.query_range(prometheus_query) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index 917c5c46db1..8b77c925705 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,8 +3,16 @@ require 'spec_helper'
describe Gitlab::Redis do
include StubENV
- before(:each) { clear_raw_config }
- after(:each) { clear_raw_config }
+ let(:config) { 'config/resque.yml' }
+
+ before(:each) do
+ stub_env('GITLAB_REDIS_CONFIG_FILE', Rails.root.join(config).to_s)
+ clear_raw_config
+ end
+
+ after(:each) do
+ clear_raw_config
+ end
describe '.params' do
subject { described_class.params }
@@ -18,22 +26,22 @@ describe Gitlab::Redis do
end
context 'when url contains unix socket reference' do
- let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
- let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
+ let(:config_old) { 'spec/fixtures/config/redis_old_format_socket.yml' }
+ let(:config_new) { 'spec/fixtures/config/redis_new_format_socket.yml' }
context 'with old format' do
- it 'returns path key instead' do
- stub_const("#{described_class}::CONFIG_FILE", config_old)
+ let(:config) { config_old }
+ it 'returns path key instead' do
is_expected.to include(path: '/path/to/old/redis.sock')
is_expected.not_to have_key(:url)
end
end
context 'with new format' do
- it 'returns path key instead' do
- stub_const("#{described_class}::CONFIG_FILE", config_new)
+ let(:config) { config_new }
+ it 'returns path key instead' do
is_expected.to include(path: '/path/to/redis.sock')
is_expected.not_to have_key(:url)
end
@@ -41,22 +49,22 @@ describe Gitlab::Redis do
end
context 'when url is host based' do
- let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
- let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config_old) { 'spec/fixtures/config/redis_old_format_host.yml' }
+ let(:config_new) { 'spec/fixtures/config/redis_new_format_host.yml' }
context 'with old format' do
- it 'returns hash with host, port, db, and password' do
- stub_const("#{described_class}::CONFIG_FILE", config_old)
+ let(:config) { config_old }
+ it 'returns hash with host, port, db, and password' do
is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
end
end
context 'with new format' do
- it 'returns hash with host, port, db, and password' do
- stub_const("#{described_class}::CONFIG_FILE", config_new)
+ let(:config) { config_new }
+ it 'returns hash with host, port, db, and password' do
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
end
@@ -74,15 +82,13 @@ describe Gitlab::Redis do
end
context 'when yml file with env variable' do
- let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') }
+ let(:config) { 'spec/fixtures/config/redis_config_with_env.yml' }
before do
stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379')
end
it 'reads redis url from env variable' do
- stub_const("#{described_class}::CONFIG_FILE", redis_config)
-
expect(described_class.url).to eq 'redis://redishost:6379'
end
end
@@ -90,14 +96,13 @@ describe Gitlab::Redis do
describe '._raw_config' do
subject { described_class._raw_config }
+ let(:config) { '/var/empty/doesnotexist' }
it 'should be frozen' do
expect(subject).to be_frozen
end
it 'returns false when the file does not exist' do
- stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
-
expect(subject).to eq(false)
end
end
@@ -134,22 +139,18 @@ describe Gitlab::Redis do
subject { described_class.new(Rails.env).sentinels }
context 'when sentinels are defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
it 'returns an array of hashes with host and port keys' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to include(host: 'localhost', port: 26380)
is_expected.to include(host: 'slave2', port: 26381)
end
end
context 'when sentinels are not defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
it 'returns nil' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_nil
end
end
@@ -159,21 +160,17 @@ describe Gitlab::Redis do
subject { described_class.new(Rails.env).sentinels? }
context 'when sentinels are defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
it 'returns true' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_truthy
end
end
context 'when sentinels are not defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
it 'returns false' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_falsey
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 089ec4e2737..ba45e2d758c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -51,8 +51,8 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('foo-') }
end
- describe 'NAMESPACE_REF_REGEX_STR' do
- subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} }
+ describe 'FULL_NAMESPACE_REGEX_STR' do
+ subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
it { is_expected.to match('gitlab.org') }
it { is_expected.to match('gitlab.org/gitlab-git') }
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
new file mode 100644
index 00000000000..a91c8655cdd
--- /dev/null
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::RequestContext, lib: true do
+ describe '#client_ip' do
+ subject { Gitlab::RequestContext.client_ip }
+ let(:app) { -> (env) {} }
+ let(:env) { Hash.new }
+
+ context 'when RequestStore::Middleware is used' do
+ around(:each) do |example|
+ RequestStore::Middleware.new(-> (env) { example.run }).call({})
+ end
+
+ context 'request' do
+ let(:ip) { '192.168.1.11' }
+
+ before do
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Gitlab::RequestContext.new(app).call(env)
+ end
+
+ it { is_expected.to eq(ip) }
+ end
+
+ context 'before RequestContext middleware run' do
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 02c139f1a0d..4f6ef3c10fc 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -155,11 +155,10 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'john@mail.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: uid }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }])
end
end
@@ -178,11 +177,10 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.email).to eql 'john@mail.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: uid }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }])
end
it 'saves successfully on subsequent tries, when both identities are present' do
@@ -204,11 +202,10 @@ describe Gitlab::Saml::User, lib: true do
local_gl_user = local_saml_user.gl_user
expect(local_gl_user).to be_valid
- expect(local_gl_user.identities.length).to eql 2
+ expect(local_gl_user.identities.length).to be 2
identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }])
end
end
end
diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
index b810c68ea03..c4b7fda5dbb 100644
--- a/spec/lib/gitlab/serializer/ci/variables_spec.rb
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::Serializer::Ci::Variables do
it 'converts keys into strings' do
is_expected.to eq([
{ key: 'key', value: 'value', public: true },
- { key: 'wee', value: 1, public: false }])
+ { key: 'wee', value: 1, public: false }
+ ])
end
end
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 0aa36a3416b..56f06b61afb 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -39,6 +39,32 @@ describe Gitlab::SidekiqStatus do
end
end
+ describe '.num_running', :redis do
+ it 'returns 0 if all jobs have been completed' do
+ expect(described_class.num_running(%w(123))).to eq(0)
+ end
+
+ it 'returns 2 if two jobs are still running' do
+ described_class.set('123')
+ described_class.set('456')
+
+ expect(described_class.num_running(%w(123 456 789))).to eq(2)
+ end
+ end
+
+ describe '.num_completed', :redis do
+ it 'returns 1 if all jobs have been completed' do
+ expect(described_class.num_completed(%w(123))).to eq(1)
+ end
+
+ it 'returns 1 if a job has not yet been completed' do
+ described_class.set('123')
+ described_class.set('456')
+
+ expect(described_class.num_completed(%w(123 456 789))).to eq(1)
+ end
+ end
+
describe '.key_for' do
it 'returns the key for a job ID' do
key = described_class.key_for('123')
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 1335a2b8f35..9213ced7b19 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::IssueTemplate do
create_template: {
user: user,
access: Gitlab::Access::MASTER,
- path: 'issue_templates' })
+ path: 'issue_templates'
+ })
end
describe '.all' do
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index 320b870309a..77dd3079e22 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::MergeRequestTemplate do
create_template: {
user: user,
access: Gitlab::Access::MASTER,
- path: 'merge_request_templates' })
+ path: 'merge_request_templates'
+ })
end
describe '.all' do
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index edadab043d7..fcfd8d58b70 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -32,7 +32,8 @@ describe Gitlab::Upgrader, lib: true do
'43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2',
'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}',
'0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4',
- 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}'])
+ 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}'
+ ])
expect(upgrader.latest_version_raw).to eq("v7.11.2")
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
new file mode 100644
index 00000000000..a504d299307
--- /dev/null
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBlocker, lib: true do
+ describe '#blocked_url?' do
+ it 'allows imports from configured web host and port' do
+ import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
+ expect(described_class.blocked_url?(import_url)).to be false
+ end
+
+ it 'allows imports from configured SSH host and port' do
+ import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
+ expect(described_class.blocked_url?(import_url)).to be false
+ end
+
+ it 'returns true for bad localhost hostname' do
+ expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for bad port' do
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+ end
+
+ it 'returns true for invalid URL' do
+ expect(described_class.blocked_url?('http://:8080')).to be true
+ end
+
+ it 'returns false for legitimate URL' do
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 2cb74629da8..fc144a2556a 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do
let(:url_sanitizer) do
described_class.new("https://github.com/me/project.git", credentials: credentials)
end
+ let(:user) { double(:user, username: 'john.doe') }
describe '.sanitize' do
def sanitize_url(url)
@@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do
end
end
+ describe '.valid?' do
+ it 'validates url strings' do
+ expect(described_class.valid?(nil)).to be(false)
+ expect(described_class.valid?('valid@project:url.git')).to be(true)
+ expect(described_class.valid?('123://invalid:url')).to be(false)
+ end
+ end
+
+ describe '.http_credentials_for_user' do
+ it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) }
+ it { expect(described_class.http_credentials_for_user('foo')).to eq({}) }
+ end
+
describe '#sanitized_url' do
it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
end
describe '#credentials' do
it { expect(url_sanitizer.credentials).to eq(credentials) }
+
+ context 'when user is given to #initialize' do
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+ end
+
+ it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) }
+ end
end
describe '#full_url' do
@@ -69,5 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do
expect(sanitizer.full_url).to eq('user@server:project.git')
end
+
+ context 'when user is given to #initialize' do
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+ end
+
+ it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") }
+ end
end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index d5d87310874..56772409989 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,5 @@
describe Gitlab::Utils, lib: true do
- def to_boolean(value)
- described_class.to_boolean(value)
- end
+ delegate :to_boolean, to: :described_class
describe '.to_boolean' do
it 'accepts booleans' do
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
new file mode 100644
index 00000000000..3255c6f1ef7
--- /dev/null
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::VisibilityLevel, lib: true do
+ describe '.level_value' do
+ it 'converts "public" to integer value' do
+ expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'converts string integer to integer value' do
+ expect(described_class.level_value('20')).to eq(20)
+ end
+
+ it 'defaults to PRIVATE when string value is not valid' do
+ expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'defaults to PRIVATE when integer value is not valid' do
+ expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index a32c6131030..8e5e8288c49 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -199,4 +199,58 @@ describe Gitlab::Workhorse, lib: true do
end
end
end
+
+ describe '.set_key_and_notify' do
+ let(:key) { 'test-key' }
+ let(:value) { 'test-value' }
+
+ subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) }
+
+ shared_examples 'set and notify' do
+ it 'set and return the same value' do
+ is_expected.to eq(value)
+ end
+
+ it 'set and notify' do
+ expect_any_instance_of(Redis).to receive(:publish)
+ .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value")
+
+ subject
+ end
+ end
+
+ context 'when we set a new key' do
+ let(:overwrite) { true }
+
+ it_behaves_like 'set and notify'
+ end
+
+ context 'when we set an existing key' do
+ let(:old_value) { 'existing-key' }
+
+ before do
+ described_class.set_key_and_notify(key, old_value, overwrite: true)
+ end
+
+ context 'and overwrite' do
+ let(:overwrite) { true }
+
+ it_behaves_like 'set and notify'
+ end
+
+ context 'and do not overwrite' do
+ let(:overwrite) { false }
+
+ it 'try to set but return the previous value' do
+ is_expected.to eq(old_value)
+ end
+
+ it 'does not notify' do
+ expect_any_instance_of(Redis).not_to receive(:publish)
+
+ subject
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 5ccf1100898..4b5938edeb9 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -13,8 +13,7 @@ describe Mattermost::Command do
describe '#create' do
let(:params) do
{ team_id: 'abc',
- trigger: 'gitlab'
- }
+ trigger: 'gitlab' }
end
subject { described_class.new(nil).create(params) }
@@ -24,7 +23,8 @@ describe Mattermost::Command do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
with(body: {
team_id: 'abc',
- trigger: 'gitlab' }.to_json).
+ trigger: 'gitlab'
+ }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 2d14be6bcc2..ac493fdb20f 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -13,19 +13,20 @@ describe Mattermost::Team do
context 'for valid request' do
let(:response) do
- [{
- "id" => "xiyro8huptfhdndadpz8r3wnbo",
- "create_at" => 1482174222155,
- "update_at" => 1482174222155,
- "delete_at" => 0,
- "display_name" => "chatops",
- "name" => "chatops",
- "email" => "admin@example.com",
- "type" => "O",
- "company_name" => "",
- "allowed_domains" => "",
- "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
- "allow_open_invite" => false }]
+ { "xiyro8huptfhdndadpz8r3wnbo" => {
+ "id" => "xiyro8huptfhdndadpz8r3wnbo",
+ "create_at" => 1482174222155,
+ "update_at" => 1482174222155,
+ "delete_at" => 0,
+ "display_name" => "chatops",
+ "name" => "chatops",
+ "email" => "admin@example.com",
+ "type" => "O",
+ "company_name" => "",
+ "allowed_domains" => "",
+ "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+ "allow_open_invite" => false
+ } }
end
before do
@@ -38,7 +39,7 @@ describe Mattermost::Team do
end
it 'returns a token' do
- is_expected.to eq(response)
+ is_expected.to eq(response.values)
end
end
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
deleted file mode 100644
index d968096783c..00000000000
--- a/spec/mailers/emails/builds_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'spec_helper'
-require 'email_spec'
-
-describe Notify do
- include EmailSpec::Matchers
-
- include_context 'gitlab email notification'
-
- describe 'build notification email' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
- shared_examples 'build email' do
- it 'contains name of project' do
- is_expected.to have_body_text build.project_name
- end
-
- it 'contains link to project' do
- is_expected.to have_body_text namespace_project_path(project.namespace, project)
- end
- end
-
- shared_examples 'an email with X-GitLab headers containing build details' do
- it 'has X-GitLab-Build* headers' do
- is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
- is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
- end
- end
-
- describe 'build success' do
- subject { Notify.build_success_email(build.id, 'wow@example.com') }
- before { build.success }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'success'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build success for/
- end
- end
-
- describe 'build fail' do
- subject { Notify.build_fail_email(build.id, 'wow@example.com') }
- before { build.drop }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build failed for/
- end
- end
- end
-end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index b692142713f..6ee91576676 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -8,6 +8,15 @@ describe Notify do
include_context 'gitlab email notification'
+ def have_referable_subject(referable, reply: false)
+ prefix = referable.project.name if referable.project
+ prefix = "Re: #{prefix}" if reply
+
+ suffix = "#{referable.title} (#{referable.to_reference})"
+
+ have_subject [prefix, suffix].compact.join(' | ')
+ end
+
context 'for a project' do
describe 'items that are assignable, the email' do
let(:current_user) { create(:user, email: "current@email.com") }
@@ -41,11 +50,11 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
+ is_expected.to have_referable_subject(issue)
end
it 'contains a link to the new issue' do
- is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
end
context 'when enabled email_author_in_body' do
@@ -54,8 +63,8 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text issue.author_name
- is_expected.to have_body_text /wrote\:/
+ is_expected.to have_html_escaped_body_text issue.author_name
+ is_expected.to have_body_text 'wrote:'
end
end
end
@@ -66,7 +75,7 @@ describe Notify do
it_behaves_like 'it should show Gmail Actions View Issue link'
it 'contains the description' do
- is_expected.to have_body_text /#{issue_with_description.description}/
+ is_expected.to have_html_escaped_body_text issue_with_description.description
end
end
@@ -87,19 +96,19 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+ is_expected.to have_referable_subject(issue, reply: true)
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text /#{previous_assignee.name}/
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text /#{assignee.name}/
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the issue' do
- is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
end
end
@@ -121,15 +130,15 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+ is_expected.to have_referable_subject(issue, reply: true)
end
it 'contains the names of the added labels' do
- is_expected.to have_body_text /foo, bar, and baz/
+ is_expected.to have_body_text 'foo, bar, and baz'
end
it 'contains a link to the issue' do
- is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
end
end
@@ -150,19 +159,19 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i
+ is_expected.to have_referable_subject(issue, reply: true)
end
it 'contains the new status' do
- is_expected.to have_body_text /#{status}/i
+ is_expected.to have_body_text status
end
it 'contains the user name' do
- is_expected.to have_body_text /#{current_user.name}/i
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the issue' do
- is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue)
end
end
@@ -181,7 +190,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i
+ is_expected.to have_referable_subject(issue, reply: true)
end
it 'contains link to new issue' do
@@ -191,7 +200,7 @@ describe Notify do
end
it 'contains a link to the original issue' do
- is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
+ is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue)
end
end
end
@@ -212,19 +221,19 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+ is_expected.to have_referable_subject(merge_request)
end
it 'contains a link to the new merge request' do
- is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/
+ is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'contains the source branch for the merge request' do
- is_expected.to have_body_text /#{merge_request.source_branch}/
+ is_expected.to have_body_text merge_request.source_branch
end
it 'contains the target branch for the merge request' do
- is_expected.to have_body_text /#{merge_request.target_branch}/
+ is_expected.to have_body_text merge_request.target_branch
end
context 'when enabled email_author_in_body' do
@@ -233,8 +242,8 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text merge_request.author_name
- is_expected.to have_body_text /wrote\:/
+ is_expected.to have_html_escaped_body_text merge_request.author_name
+ is_expected.to have_body_text 'wrote:'
end
end
end
@@ -246,7 +255,7 @@ describe Notify do
it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
- is_expected.to have_body_text /#{merge_request_with_description.description}/
+ is_expected.to have_html_escaped_body_text merge_request_with_description.description
end
end
@@ -267,19 +276,19 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+ is_expected.to have_referable_subject(merge_request, reply: true)
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text /#{previous_assignee.name}/
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text /#{assignee.name}/
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the merge request' do
- is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
@@ -301,15 +310,15 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+ is_expected.to have_referable_subject(merge_request, reply: true)
end
it 'contains the names of the added labels' do
- is_expected.to have_body_text /foo, bar, and baz/
+ is_expected.to have_body_text 'foo, bar, and baz'
end
it 'contains a link to the merge request' do
- is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
@@ -330,19 +339,19 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i
+ is_expected.to have_referable_subject(merge_request, reply: true)
end
it 'contains the new status' do
- is_expected.to have_body_text /#{status}/i
+ is_expected.to have_body_text status
end
it 'contains the user name' do
- is_expected.to have_body_text /#{current_user.name}/i
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the merge request' do
- is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
@@ -363,15 +372,15 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+ is_expected.to have_referable_subject(merge_request, reply: true)
end
it 'contains the new status' do
- is_expected.to have_body_text /merged/i
+ is_expected.to have_body_text 'merged'
end
it 'contains a link to the merge request' do
- is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
+ is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
end
@@ -387,15 +396,15 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
- is_expected.to have_subject /Project was moved/
+ is_expected.to have_subject "#{project.name} | Project was moved"
end
it 'contains name of project' do
- is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
end
it 'contains new user role' do
- is_expected.to have_body_text /#{project.ssh_url_to_repo}/
+ is_expected.to have_body_text project.ssh_url_to_repo
end
end
@@ -424,9 +433,9 @@ describe Notify do
expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
- is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
+ is_expected.to have_body_text project_member.human_access
end
end
@@ -451,9 +460,9 @@ describe Notify do
expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
- is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
+ is_expected.to have_body_text project_member.human_access
end
end
end
@@ -473,13 +482,14 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text project.web_url
end
end
describe 'project access changed' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:owner) { create(:user, name: "Chang O'Keefe") }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -490,9 +500,9 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{project.web_url}/
- is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project_member.human_access
end
end
@@ -521,10 +531,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{project.web_url}/
- is_expected.to have_body_text /#{project_member.human_access}/
- is_expected.to have_body_text /#{project_member.invite_token}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project_member.human_access
+ is_expected.to have_body_text project_member.invite_token
end
end
@@ -546,10 +556,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{project.web_url}/
- is_expected.to have_body_text /#{project_member.invite_email}/
- is_expected.to have_body_text /#{invited_user.name}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project_member.invite_email
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -570,9 +580,9 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{project.web_url}/
- is_expected.to have_body_text /#{project_member.invite_email}/
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project_member.invite_email
end
end
@@ -598,11 +608,11 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text /#{note.note}/
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
- is_expected.not_to have_body_text /wrote\:/
+ is_expected.not_to have_body_text 'wrote:'
end
context 'when enabled email_author_in_body' do
@@ -611,8 +621,8 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
- is_expected.to have_body_text /wrote\:/
+ is_expected.to have_html_escaped_body_text note.author_name
+ is_expected.to have_body_text 'wrote:'
end
end
end
@@ -632,7 +642,7 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
- is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/
+ is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})"
end
it 'contains a link to the commit' do
@@ -655,11 +665,11 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
+ is_expected.to have_referable_subject(merge_request, reply: true)
end
it 'contains a link to the merge request note' do
- is_expected.to have_body_text /#{note_on_merge_request_path}/
+ is_expected.to have_body_text note_on_merge_request_path
end
end
@@ -678,11 +688,11 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
+ is_expected.to have_referable_subject(issue, reply: true)
end
it 'contains a link to the issue note' do
- is_expected.to have_body_text /#{note_on_issue_path}/
+ is_expected.to have_body_text note_on_issue_path
end
end
end
@@ -698,11 +708,11 @@ describe Notify do
let(:note) { create(model, project: project, author: note_author) }
it "includes diffs with character-level highlighting" do
- is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/
+ is_expected.to have_body_text '<span class="p">}</span></span>'
end
it 'contains a link to the diff file' do
- is_expected.to have_body_text /#{note.diff_file.file_path}/
+ is_expected.to have_body_text note.diff_file.file_path
end
it_behaves_like 'it should have Gmail Actions links'
@@ -718,11 +728,11 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text /#{note.note}/
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
- is_expected.not_to have_body_text /wrote\:/
+ is_expected.not_to have_body_text 'wrote:'
end
context 'when enabled email_author_in_body' do
@@ -731,8 +741,8 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
- is_expected.to have_body_text /wrote\:/
+ is_expected.to have_html_escaped_body_text note.author_name
+ is_expected.to have_body_text 'wrote:'
end
end
end
@@ -777,9 +787,9 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Request to join the #{group.name} group"
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group_group_members_url(group)}/
- is_expected.to have_body_text /#{group_member.human_access}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group_group_members_url(group)
+ is_expected.to have_body_text group_member.human_access
end
end
@@ -798,8 +808,8 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.web_url
end
end
@@ -816,9 +826,9 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group.web_url}/
- is_expected.to have_body_text /#{group_member.human_access}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_member.human_access
end
end
@@ -847,10 +857,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group.web_url}/
- is_expected.to have_body_text /#{group_member.human_access}/
- is_expected.to have_body_text /#{group_member.invite_token}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_member.human_access
+ is_expected.to have_body_text group_member.invite_token
end
end
@@ -872,10 +882,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group.web_url}/
- is_expected.to have_body_text /#{group_member.invite_email}/
- is_expected.to have_body_text /#{invited_user.name}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_member.invite_email
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -896,9 +906,9 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text /#{group.name}/
- is_expected.to have_body_text /#{group.web_url}/
- is_expected.to have_body_text /#{group_member.invite_email}/
+ is_expected.to have_html_escaped_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_member.invite_email
end
end
end
@@ -925,11 +935,11 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /^Confirmation instructions/
+ is_expected.to have_subject 'Confirmation instructions | A Nice Suffix'
end
it 'includes a link to the site' do
- is_expected.to have_body_text /#{example_site_path}/
+ is_expected.to have_body_text example_site_path
end
end
@@ -952,11 +962,11 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /Pushed new branch master/
+ is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master"
end
it 'contains a link to the branch' do
- is_expected.to have_body_text /#{tree_path}/
+ is_expected.to have_body_text tree_path
end
end
@@ -979,11 +989,11 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /Pushed new tag v1\.0/
+ is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0"
end
it 'contains a link to the tag' do
- is_expected.to have_body_text /#{tree_path}/
+ is_expected.to have_body_text tree_path
end
end
@@ -1005,7 +1015,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /Deleted branch master/
+ is_expected.to have_subject "[Git][#{project.full_path}] Deleted branch master"
end
end
@@ -1027,7 +1037,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /Deleted tag v1\.0/
+ is_expected.to have_subject "[Git][#{project.full_path}] Deleted tag v1.0"
end
end
@@ -1055,23 +1065,23 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/
+ is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified"
end
it 'includes commits list' do
- is_expected.to have_body_text /Change some files/
+ is_expected.to have_body_text 'Change some files'
end
it 'includes diffs with character-level highlighting' do
- is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
+ is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex'
end
it 'contains a link to the diff' do
- is_expected.to have_body_text /#{diff_path}/
+ is_expected.to have_body_text diff_path
end
it 'does not contain the misleading footer' do
- is_expected.not_to have_body_text /you are a member of/
+ is_expected.not_to have_body_text 'you are a member of'
end
context "when set to send from committer email if domain matches" do
@@ -1157,19 +1167,19 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{commits.first.title}/
+ is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}"
end
it 'includes commits list' do
- is_expected.to have_body_text /Change some files/
+ is_expected.to have_body_text 'Change some files'
end
it 'includes diffs with character-level highlighting' do
- is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
+ is_expected.to have_body_text 'def</span> <span class="nf">archive_formats_regex'
end
it 'contains a link to the diff' do
- is_expected.to have_body_text /#{diff_path}/
+ is_expected.to have_body_text diff_path
end
end
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
new file mode 100644
index 00000000000..57eb03e3c80
--- /dev/null
+++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe MigrateBuildEventsToPipelineEvents, truncate: true do
+ let(:migration) { described_class.new }
+ let(:project_with_pipeline_service) { create(:empty_project) }
+ let(:project_with_build_service) { create(:empty_project) }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services (properties, build_events, pipeline_events, type)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
+ ;
+ SQL
+
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services
+ (properties, build_events, pipeline_events, type, project_id)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_pipelines":true}', false, true,
+ 'PipelinesEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_build_service.id})
+ ;
+ SQL
+ end
+
+ describe '#up' do
+ before do
+ silence_migration = Module.new do
+ # rubocop:disable Rails/Delegate
+ def execute(query)
+ connection.execute(query)
+ end
+ end
+
+ migration.extend(silence_migration)
+ migration.up
+ end
+
+ it 'migrates chat service properly' do
+ [SlackService, MattermostService, HipchatService].each do |service|
+ expect(service.count).to eq(1)
+
+ verify_service_record(service.first)
+ end
+ end
+
+ it 'migrates pipelines email service only if it has none before' do
+ Project.find_each do |project|
+ pipeline_service_count =
+ project.services.where(type: 'PipelinesEmailService').count
+
+ expect(pipeline_service_count).to eq(1)
+
+ verify_service_record(project.pipelines_email_service)
+ end
+ end
+
+ def verify_service_record(service)
+ expect(service.notify_only_broken_pipelines).to be(true)
+ expect(service.build_events).to be(false)
+ expect(service.pipeline_events).to be(true)
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 6a93deb5412..b6d678bac18 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -62,7 +62,7 @@ describe MigrateProcessCommitWorkerJobs do
end
def pop_job
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
end
before do
@@ -198,7 +198,7 @@ describe MigrateProcessCommitWorkerJobs do
let(:job) do
migration.down
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
end
it 'includes the project ID' do
diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb
new file mode 100644
index 00000000000..36e82729c23
--- /dev/null
+++ b/spec/migrations/rename_more_reserved_project_names_spec.rb
@@ -0,0 +1,47 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserved_project_names.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe RenameMoreReservedProjectNames, truncate: true do
+ let(:migration) { described_class.new }
+ let!(:project) { create(:empty_project) }
+
+ before do
+ project.path = 'artifacts'
+ project.save!(validate: false)
+ end
+
+ describe '#up' do
+ context 'when project repository exists' do
+ before { project.create_repository }
+
+ context 'when no exception is raised' do
+ it 'renames project with reserved names' do
+ migration.up
+
+ expect(project.reload.path).to eq('artifacts0')
+ end
+ end
+
+ context 'when exception is raised during rename' do
+ before do
+ allow(project).to receive(:rename_repo).and_raise(StandardError)
+ end
+
+ it 'captures exception from project rename' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when project repository does not exist' do
+ it 'does not raise error' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 30f8fdf91b2..92d70cfc64c 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,12 @@
require 'spec_helper'
describe Ability, lib: true do
+ context 'using a nil subject' do
+ it 'is always empty' do
+ expect(Ability.allowed(nil, nil).to_set).to be_empty
+ end
+ end
+
describe '.can_edit_note?' do
let(:project) { create(:empty_project) }
let(:note) { create(:note_on_issue, project: project) }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index c4486a32082..4e71597521d 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe AbuseReport, type: :model do
subject { create(:abuse_report) }
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
it { expect(subject).to be_valid }
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 0b72a2f979b..1060bf3cbf4 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -7,4 +7,6 @@ RSpec.describe Appearance, type: :model do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:description) }
+
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b950fcdd81a..01ca1584ed2 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -29,6 +29,40 @@ describe ApplicationSetting, models: true do
it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
end
+ describe 'default_artifacts_expire_in' do
+ it 'sets an error if it cannot parse' do
+ setting.update(default_artifacts_expire_in: 'a')
+
+ expect_invalid
+ end
+
+ it 'sets an error if it is blank' do
+ setting.update(default_artifacts_expire_in: ' ')
+
+ expect_invalid
+ end
+
+ it 'sets the value if it is valid' do
+ setting.update(default_artifacts_expire_in: '30 days')
+
+ expect(setting).to be_valid
+ expect(setting.default_artifacts_expire_in).to eq('30 days')
+ end
+
+ it 'sets the value if it is 0' do
+ setting.update(default_artifacts_expire_in: '0')
+
+ expect(setting).to be_valid
+ expect(setting.default_artifacts_expire_in).to eq('0')
+ end
+
+ def expect_invalid
+ expect(setting).to be_invalid
+ expect(setting.errors.messages)
+ .to have_key(:default_artifacts_expire_in)
+ end
+ end
+
it { is_expected.to validate_presence_of(:max_attachment_size) }
it do
@@ -62,9 +96,9 @@ describe ApplicationSetting, models: true do
describe 'inclusion' do
it { is_expected.to allow_value('custom1').for(:repository_storages) }
- it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) }
+ it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) }
it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
- it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) }
+ it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) }
end
describe 'presence' do
@@ -83,7 +117,7 @@ describe ApplicationSetting, models: true do
describe '#repository_storage' do
it 'returns the first storage' do
- setting.repository_storages = ['good', 'bad']
+ setting.repository_storages = %w(good bad)
expect(setting.repository_storage).to eq('good')
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 03d02b4d382..94c25a454aa 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -70,6 +70,8 @@ describe Blob do
end
describe '#to_partial_path' do
+ let(:project) { double(lfs_enabled?: true) }
+
def stubbed_blob(overrides = {})
overrides.reverse_merge!(
image?: false,
@@ -84,34 +86,35 @@ describe Blob do
end
end
- it 'handles LFS pointers' do
- blob = stubbed_blob(lfs_pointer?: true)
+ it 'handles LFS pointers with LFS enabled' do
+ blob = stubbed_blob(lfs_pointer?: true, text?: true)
+ expect(blob.to_partial_path(project)).to eq 'download'
+ end
- expect(blob.to_partial_path).to eq 'download'
+ it 'handles LFS pointers with LFS disabled' do
+ blob = stubbed_blob(lfs_pointer?: true, text?: true)
+ project = double(lfs_enabled?: false)
+ expect(blob.to_partial_path(project)).to eq 'text'
end
it 'handles SVGs' do
blob = stubbed_blob(text?: true, svg?: true)
-
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'image'
end
it 'handles images' do
blob = stubbed_blob(image?: true)
-
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.to_partial_path(project)).to eq 'image'
end
it 'handles text' do
blob = stubbed_blob(text?: true)
-
- expect(blob.to_partial_path).to eq 'text'
+ expect(blob.to_partial_path(project)).to eq 'text'
end
it 'defaults to download' do
blob = stubbed_blob
-
- expect(blob.to_partial_path).to eq 'download'
+ expect(blob.to_partial_path(project)).to eq 'download'
end
end
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
new file mode 100644
index 00000000000..5283561a83f
--- /dev/null
+++ b/spec/models/chat_team_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ChatTeam, type: :model do
+ subject { create(:chat_team) }
+
+ # Associations
+ it { is_expected.to belong_to(:namespace) }
+
+ # Validations
+ it { is_expected.to validate_uniqueness_of(:namespace) }
+
+ # Fields
+ it { is_expected.to respond_to(:name) }
+ it { is_expected.to respond_to(:team_id) }
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 83a2efb55b9..ac47b34b6fc 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -20,6 +20,30 @@ describe Ci::Build, :models do
it { is_expected.to validate_presence_of :ref }
it { is_expected.to respond_to :trace_html }
+ describe '#actionize' do
+ context 'when build is a created' do
+ before do
+ build.update_column(:status, :created)
+ end
+
+ it 'makes build a manual action' do
+ expect(build.actionize).to be true
+ expect(build.reload).to be_manual
+ end
+ end
+
+ context 'when build is not created' do
+ before do
+ build.update_column(:status, :pending)
+ end
+
+ it 'does not change build status' do
+ expect(build.actionize).to be false
+ expect(build.reload).to be_pending
+ end
+ end
+ end
+
describe '#any_runners_online?' do
subject { build.any_runners_online? }
@@ -162,11 +186,17 @@ describe Ci::Build, :models do
is_expected.to be_nil
end
- it 'when resseting value' do
+ it 'when resetting value' do
build.artifacts_expire_in = nil
is_expected.to be_nil
end
+
+ it 'when setting to 0' do
+ build.artifacts_expire_in = '0'
+
+ is_expected.to be_nil
+ end
end
describe '#commit' do
@@ -175,20 +205,6 @@ describe Ci::Build, :models do
end
end
- describe '#create_from' do
- before do
- build.status = 'success'
- build.save
- end
- let(:create_from_build) { Ci::Build.create_from build }
-
- it 'exists a pending task' do
- expect(Ci::Build.pending.count(:all)).to eq 0
- create_from_build
- expect(Ci::Build.pending.count(:all)).to be > 0
- end
- end
-
describe '#depends_on_builds' do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
@@ -329,11 +345,11 @@ describe Ci::Build, :models do
describe '#expanded_environment_name' do
subject { build.expanded_environment_name }
- context 'when environment uses $CI_BUILD_REF_NAME' do
+ context 'when environment uses $CI_COMMIT_REF_NAME' do
let(:build) do
create(:ci_build,
ref: 'master',
- environment: 'review/$CI_BUILD_REF_NAME')
+ environment: 'review/$CI_COMMIT_REF_NAME')
end
it { is_expected.to eq('review/master') }
@@ -595,13 +611,21 @@ describe Ci::Build, :models do
it { is_expected.to be_falsey }
end
- context 'and build.status is failed' do
+ context 'and build status is failed' do
before do
build.status = 'failed'
end
it { is_expected.to be_truthy }
end
+
+ context 'when build is a manual action' do
+ before do
+ build.status = 'manual'
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
end
@@ -690,12 +714,12 @@ describe Ci::Build, :models do
end
end
- describe '#manual?' do
+ describe '#action?' do
before do
build.update(when: value)
end
- subject { build.manual? }
+ subject { build.action? }
context 'when is set to manual' do
let(:value) { 'manual' }
@@ -711,14 +735,50 @@ describe Ci::Build, :models do
end
end
+ describe '#has_commands?' do
+ context 'when build has commands' do
+ let(:build) do
+ create(:ci_build, commands: 'rspec')
+ end
+
+ it 'has commands' do
+ expect(build).to have_commands
+ end
+ end
+
+ context 'when does not have commands' do
+ context 'when commands are an empty string' do
+ let(:build) do
+ create(:ci_build, commands: '')
+ end
+
+ it 'has no commands' do
+ expect(build).not_to have_commands
+ end
+ end
+
+ context 'when commands are not set at all' do
+ let(:build) do
+ create(:ci_build, commands: nil)
+ end
+
+ it 'has no commands' do
+ expect(build).not_to have_commands
+ end
+ end
+ end
+ end
+
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
+
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
subject { create(:ci_build, tag_list: []) }
+
it { is_expected.not_to have_tags }
end
end
@@ -735,8 +795,8 @@ describe Ci::Build, :models do
describe '#merge_request' do
def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
+ create(factory, source_project: pipeline.project,
+ target_project: pipeline.project,
source_branch: build.ref,
created_at: created_at)
end
@@ -855,7 +915,7 @@ describe Ci::Build, :models do
end
context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") }
it { is_expected.to eq(@environment) }
end
@@ -1226,23 +1286,25 @@ describe Ci::Build, :models do
[
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
- { key: 'CI_BUILD_REF', value: build.sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
- { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+ { key: 'CI_JOB_NAME', value: 'test', public: true },
+ { key: 'CI_JOB_STAGE', value: 'test', public: true },
+ { key: 'CI_JOB_TOKEN', value: build.token, public: false },
+ { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
+ { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
+ { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true },
- { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
]
end
@@ -1257,13 +1319,13 @@ describe Ci::Build, :models do
build.yaml_variables = []
end
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when build has user' do
let(:user_variables) do
- [ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } ]
+ [{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }]
end
before do
@@ -1295,7 +1357,7 @@ describe Ci::Build, :models do
end
let(:manual_variable) do
- { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+ { key: 'CI_JOB_MANUAL', value: 'true', public: true }
end
it { is_expected.to include(manual_variable) }
@@ -1303,7 +1365,7 @@ describe Ci::Build, :models do
context 'when build is for tag' do
let(:tag_variable) do
- { key: 'CI_BUILD_TAG', value: 'master', public: true }
+ { key: 'CI_COMMIT_TAG', value: 'master', public: true }
end
before do
@@ -1332,7 +1394,7 @@ describe Ci::Build, :models do
{ key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
end
let(:predefined_trigger_variable) do
- { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
end
before do
@@ -1356,7 +1418,7 @@ describe Ci::Build, :models do
context 'when config is not found' do
let(:config) { nil }
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when config does not have a questioned job' do
@@ -1368,7 +1430,7 @@ describe Ci::Build, :models do
})
end
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when config has variables' do
@@ -1386,7 +1448,8 @@ describe Ci::Build, :models do
[{ key: 'KEY', value: 'value', public: true }]
end
- it { is_expected.to eq(predefined_variables + variables) }
+ it { is_expected.to include(*predefined_variables) }
+ it { is_expected.to include(*variables) }
end
end
end
@@ -1420,7 +1483,7 @@ describe Ci::Build, :models do
end
context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
before do
build.update(runner: runner)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 10c2bfbb400..53282b999dc 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -24,6 +24,14 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ describe '#block' do
+ it 'changes pipeline status to manual' do
+ expect(pipeline.block).to be true
+ expect(pipeline.reload).to be_manual
+ expect(pipeline.reload).to be_blocked
+ end
+ end
+
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
@@ -168,9 +176,9 @@ describe Ci::Pipeline, models: true do
end
it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([['build', 'failed'],
- ['test', 'success'],
- ['deploy', 'running']])
+ expect(statuses).to eq([%w(build failed),
+ %w(test success),
+ %w(deploy running)])
end
context 'when commit status is retried' do
@@ -183,12 +191,30 @@ describe Ci::Pipeline, models: true do
end
it 'ignores the previous state' do
- expect(statuses).to eq([['build', 'success'],
- ['test', 'success'],
- ['deploy', 'running']])
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
end
end
end
+
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
+
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
+ end
end
describe '#stages_count' do
@@ -199,7 +225,7 @@ describe Ci::Pipeline, models: true do
describe '#stages_name' do
it 'returns a valid names of stages' do
- expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
+ expect(pipeline.stages_name).to eq(%w(build test deploy))
end
end
end
@@ -506,6 +532,19 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '.latest_successful_for_refs' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
+ let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+
+ it 'returns the latest successful pipeline for both refs' do
+ refs = %w(ref1 ref2 ref3)
+
+ expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 })
+ end
+ end
+
describe '#status' do
let(:build) do
create(:ci_build, :created, pipeline: pipeline, name: 'test')
@@ -635,6 +674,14 @@ describe Ci::Pipeline, models: true do
end
end
+ context 'when pipeline is blocked' do
+ let(:pipeline) { create(:ci_pipeline, status: :manual) }
+
+ it 'returns detailed status for blocked pipeline' do
+ expect(subject.text).to eq 'blocked'
+ end
+ end
+
context 'when pipeline is successful but with warnings' do
let(:pipeline) { create(:ci_pipeline, status: :success) }
@@ -767,7 +814,7 @@ describe Ci::Pipeline, models: true do
end
it 'cancels created builds' do
- expect(latest_status).to eq ['canceled', 'canceled']
+ expect(latest_status).to eq %w(canceled canceled)
end
end
end
@@ -984,6 +1031,19 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#update_status' do
+ let(:pipeline) { create(:ci_pipeline, sha: '123456') }
+
+ it 'updates the cached status' do
+ fake_status = double
+ # after updating the status, the status is set to `skipped` for this pipeline's builds
+ expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
+ expect(fake_status).to receive(:store_in_cache_if_needed)
+
+ pipeline.update_status
+ end
+ end
+
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/models/ci/pipeline_status_spec.rb
new file mode 100644
index 00000000000..bc5b71666c2
--- /dev/null
+++ b/spec/models/ci/pipeline_status_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Ci::PipelineStatus do
+ let(:project) { create(:project) }
+ let(:pipeline_status) { described_class.new(project) }
+
+ describe '.load_for_project' do
+ it "loads the status" do
+ expect_any_instance_of(described_class).to receive(:load_status)
+
+ described_class.load_for_project(project)
+ end
+ end
+
+ describe '#has_status?' do
+ it "is false when the status wasn't loaded yet" do
+ expect(pipeline_status.has_status?).to be_falsy
+ end
+
+ it 'is true when all status information was loaded' do
+ fake_commit = double
+ allow(fake_commit).to receive(:status).and_return('failed')
+ allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
+ allow(pipeline_status).to receive(:commit).and_return(fake_commit)
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+ pipeline_status.load_status
+
+ expect(pipeline_status.has_status?).to be_truthy
+ end
+ end
+
+ describe '#load_status' do
+ it 'loads the status from the cache when there is one' do
+ expect(pipeline_status).to receive(:has_cache?).and_return(true)
+ expect(pipeline_status).to receive(:load_from_cache)
+
+ pipeline_status.load_status
+ end
+
+ it 'loads the status from the project commit when there is no cache' do
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+ expect(pipeline_status).to receive(:load_from_commit)
+
+ pipeline_status.load_status
+ end
+
+ it 'stores the status in the cache when it loading it from the project' do
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+ allow(pipeline_status).to receive(:load_from_commit)
+
+ expect(pipeline_status).to receive(:store_in_cache)
+
+ pipeline_status.load_status
+ end
+
+ it 'sets the state to loaded' do
+ pipeline_status.load_status
+
+ expect(pipeline_status).to be_loaded
+ end
+
+ it 'only loads the status once' do
+ expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
+ expect(pipeline_status).to receive(:load_from_cache).exactly(1)
+
+ pipeline_status.load_status
+ pipeline_status.load_status
+ end
+ end
+
+ describe "#load_from_commit" do
+ let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+ it 'reads the status from the pipeline for the commit' do
+ pipeline_status.load_from_commit
+
+ expect(pipeline_status.status).to eq('success')
+ expect(pipeline_status.sha).to eq(project.commit.sha)
+ end
+
+ it "doesn't fail for an empty project" do
+ status_for_empty_commit = described_class.new(create(:empty_project))
+
+ status_for_empty_commit.load_status
+
+ expect(status_for_empty_commit).to be_loaded
+ end
+ end
+
+ describe "#store_in_cache", :redis do
+ it "sets the object in redis" do
+ pipeline_status.sha = '123456'
+ pipeline_status.status = 'failed'
+
+ pipeline_status.store_in_cache
+ read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(read_sha).to eq('123456')
+ expect(read_status).to eq('failed')
+ end
+ end
+
+ describe '#store_in_cache_if_needed', :redis do
+ it 'stores the state in the cache when the sha is the HEAD of the project' do
+ create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+ build_status = described_class.load_for_project(project)
+
+ build_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(sha).not_to be_nil
+ expect(status).not_to be_nil
+ end
+
+ it "doesn't store the status in redis when the sha is not the head of the project" do
+ other_status = described_class.new(project, sha: "123456", status: "failed")
+
+ other_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(sha).to be_nil
+ expect(status).to be_nil
+ end
+
+ it "deletes the cache if the repository doesn't have a head commit" do
+ empty_project = create(:empty_project)
+ Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
+ other_status = described_class.new(empty_project, sha: "123456", status: "failed")
+
+ other_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
+
+ expect(sha).to be_nil
+ expect(status).to be_nil
+ end
+ end
+
+ describe "with a status in redis", :redis do
+ let(:status) { 'success' }
+ let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+
+ before do
+ Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
+ end
+
+ describe '#load_from_cache' do
+ it 'reads the status from redis' do
+ pipeline_status.load_from_cache
+
+ expect(pipeline_status.sha).to eq(sha)
+ expect(pipeline_status.status).to eq(status)
+ end
+ end
+
+ describe '#has_cache?' do
+ it 'knows the status is cached' do
+ expect(pipeline_status.has_cache?).to be_truthy
+ end
+ end
+
+ describe '#delete_from_cache' do
+ it 'deletes values from redis' do
+ pipeline_status.delete_from_cache
+
+ key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
+
+ expect(key_exists).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index f8513ac8b1c..76ce558eea0 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -113,7 +113,7 @@ describe Ci::Runner, models: true do
context 'when runner has tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
end
shared_examples 'tagged build picker' do
@@ -169,7 +169,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
end
it 'cannot handle it for builds without matching tags' do
@@ -189,7 +189,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
@@ -212,7 +212,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index c4a9743a4e2..c38faf32f7d 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -170,22 +170,31 @@ describe Ci::Stage, models: true do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
- let(:stage) { build(:ci_stage, warnings: true) }
+ let(:stage) { build(:ci_stage, warnings: 2) }
- it 'has memoized warnings' do
+ it 'returns true using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).to have_warnings
end
end
context 'when there are no warnings' do
- let(:stage) { build(:ci_stage, warnings: false) }
+ let(:stage) { build(:ci_stage, warnings: 0) }
- it 'has memoized warnings' do
+ it 'returns false using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
+
+ context 'when number of warnings is not a valid value' do
+ let(:stage) { build(:ci_stage, warnings: true) }
+
+ it 'calculates statuses using database queries' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).not_to have_warnings
+ end
+ end
end
context 'when calculating warnings from statuses' do
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 3ca9231f58e..1bcb673cb16 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,17 +1,83 @@
require 'spec_helper'
describe Ci::Trigger, models: true do
- let(:project) { FactoryGirl.create :empty_project }
+ let(:project) { create :empty_project }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:owner) }
+ it { is_expected.to have_many(:trigger_requests) }
+ end
describe 'before_validation' do
it 'sets an random token if none provided' do
- trigger = FactoryGirl.create :ci_trigger_without_token, project: project
+ trigger = create(:ci_trigger_without_token, project: project)
+
expect(trigger.token).not_to be_nil
end
it 'does not set an random token if one provided' do
- trigger = FactoryGirl.create :ci_trigger, project: project
+ trigger = create(:ci_trigger, project: project)
+
expect(trigger.token).to eq('token')
end
end
+
+ describe '#short_token' do
+ let(:trigger) { create(:ci_trigger, token: '12345678') }
+
+ subject { trigger.short_token }
+
+ it 'returns shortened token' do
+ is_expected.to eq('1234')
+ end
+ end
+
+ describe '#legacy?' do
+ let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
+
+ subject { trigger }
+
+ context 'when owner is blank' do
+ let(:owner) { nil }
+
+ it { is_expected.to be_legacy }
+ end
+
+ context 'when owner is set' do
+ let(:owner) { create(:user) }
+
+ it { is_expected.not_to be_legacy }
+ end
+ end
+
+ describe '#can_access_project?' do
+ let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
+
+ context 'when owner is blank' do
+ let(:owner) { nil }
+
+ subject { trigger.can_access_project? }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when owner is set' do
+ let(:owner) { create(:user) }
+
+ subject { trigger.can_access_project? }
+
+ context 'and is member of the project' do
+ before do
+ project.team << [owner, :developer]
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'and is not member of the project' do
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index bee9f714849..048d25869bc 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable, models: true do
let(:secret_value) { 'secret' }
it { is_expected.to validate_presence_of(:key) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:key).is_at_most(255) }
it { is_expected.to allow_value('foo').for(:key) }
it { is_expected.not_to allow_value('foo bar').for(:key) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 32f9366a14c..980a1b70ef5 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -212,6 +212,25 @@ eos
end
end
+ describe '#latest_pipeline' do
+ let!(:first_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+ let!(:second_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+
+ it 'returns latest pipeline' do
+ expect(commit.latest_pipeline).to eq second_pipeline
+ end
+ end
+
describe '#status' do
context 'without ref argument' do
before do
@@ -369,4 +388,32 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
+
+ describe '#raw_diffs' do
+ context 'Gitaly commit_raw_diffs feature enabled' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+ end
+
+ context 'when a truthy deltas_only is not passed to args' do
+ it 'fetches diffs from Gitaly server' do
+ expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+ with(commit)
+
+ commit.raw_diffs
+ end
+ end
+
+ context 'when a truthy deltas_only is passed to args' do
+ it 'fetches diffs using Rugged' do
+ opts = { deltas_only: true }
+
+ expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+ expect(commit.raw).to receive(:diffs).with(opts)
+
+ commit.raw_diffs(opts)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 36533bdd11e..ea5e4e21039 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -158,7 +158,7 @@ describe CommitStatus, :models do
end
end
- describe '.exclude_ignored' do
+ describe '.after_stage' do
subject { described_class.after_stage(0) }
let(:statuses) do
@@ -185,11 +185,32 @@ describe CommitStatus, :models do
create_status(allow_failure: true, status: 'success'),
create_status(allow_failure: true, status: 'failed'),
create_status(allow_failure: false, status: 'success'),
- create_status(allow_failure: false, status: 'failed')]
+ create_status(allow_failure: false, status: 'failed'),
+ create_status(allow_failure: true, status: 'manual'),
+ create_status(allow_failure: false, status: 'manual')]
+ end
+
+ it 'returns statuses without what we want to ignore' do
+ is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
+ end
+ end
+
+ describe '.failed_but_allowed' do
+ subject { described_class.failed_but_allowed.order(:id) }
+
+ let(:statuses) do
+ [create_status(allow_failure: true, status: 'success'),
+ create_status(allow_failure: true, status: 'failed'),
+ create_status(allow_failure: false, status: 'success'),
+ create_status(allow_failure: false, status: 'failed'),
+ create_status(allow_failure: true, status: 'canceled'),
+ create_status(allow_failure: false, status: 'canceled'),
+ create_status(allow_failure: true, status: 'manual'),
+ create_status(allow_failure: false, status: 'manual')]
end
it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9))
+ is_expected.to eq(statuses.values_at(1, 4))
end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 2e3702f7520..6151d53cd91 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe CacheMarkdownField do
- CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+ caching_classes = CacheMarkdownField::CACHING_CLASSES
+ CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
@@ -54,7 +55,7 @@ describe CacheMarkdownField do
end
end
- CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+ CacheMarkdownField::CACHING_CLASSES = caching_classes
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index dbfe3cd2d36..82abad0e2f6 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -109,6 +109,42 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
+
+ context 'when one status finished and second is still created' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'when there is a manual status before created status' do
+ let!(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :manual, allow_failure: false),
+ create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'manual' }
+ end
+
+ context 'when one status is a blocking manual action' do
+ let!(:statuses) do
+ [create(type, status: :failed),
+ create(type, status: :manual, allow_failure: false)]
+ end
+
+ it { is_expected.to eq 'manual' }
+ end
+
+ context 'when one status is a non-blocking manual action' do
+ let!(:statuses) do
+ [create(type, status: :failed),
+ create(type, status: :manual, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'failed' }
+ end
end
context 'ci build statuses' do
@@ -218,6 +254,18 @@ describe HasStatus do
it_behaves_like 'not containing the job', status
end
end
+
+ describe '.manual' do
+ subject { CommitStatus.manual }
+
+ %i[manual].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success skipped canceled].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
end
describe '::DEFAULT_STATUS' do
@@ -225,4 +273,10 @@ describe HasStatus do
expect(described_class::DEFAULT_STATUS).to eq 'created'
end
end
+
+ describe '::BLOCKED_STATUS' do
+ it 'is a status manual' do
+ expect(described_class::BLOCKED_STATUS).to eq 'manual'
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 545a11912e3..9574796a945 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -278,6 +278,16 @@ describe Issue, "Issuable" do
end
end
+ context 'issue has labels' do
+ let(:labels) { [create(:label), create(:label)] }
+
+ before { issue.update_attribute(:labels, labels)}
+
+ it 'includes labels in the hook data' do
+ expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ end
+ end
+
include_examples 'project hook data'
include_examples 'deprecated repository hook data'
end
@@ -344,6 +354,46 @@ describe Issue, "Issuable" do
end
end
+ describe '.order_due_date_and_labels_priority' do
+ let(:project) { create(:empty_project) }
+
+ def create_issue(milestone, labels)
+ create(:labeled_issue, milestone: milestone, labels: labels, project: project)
+ end
+
+ it 'sorts issues in order of milestone due date, then label priority' do
+ first_priority = create(:label, project: project, priority: 1)
+ second_priority = create(:label, project: project, priority: 2)
+ no_priority = create(:label, project: project)
+
+ first_milestone = create(:milestone, project: project, due_date: Time.now)
+ second_milestone = create(:milestone, project: project, due_date: Time.now + 1.month)
+ third_milestone = create(:milestone, project: project)
+
+ # The issues here are ordered by label priority, to ensure that we don't
+ # accidentally just sort by creation date.
+ second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority])
+ third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority])
+ first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority])
+ second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority])
+ no_milestone_second_priority = create_issue(nil, [second_priority, no_priority])
+ first_milestone_no_priority = create_issue(first_milestone, [no_priority])
+ second_milestone_no_labels = create_issue(second_milestone, [])
+ third_milestone_no_priority = create_issue(third_milestone, [no_priority])
+
+ result = Issue.order_due_date_and_labels_priority
+
+ expect(result).to eq([first_milestone_second_priority,
+ first_milestone_no_priority,
+ second_milestone_first_priority,
+ second_milestone_second_priority,
+ second_milestone_no_labels,
+ third_milestone_first_priority,
+ no_milestone_second_priority,
+ third_milestone_no_priority])
+ end
+ end
+
describe '.order_labels_priority' do
let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index ad703a6c8bb..68e4c0a522b 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -116,21 +116,41 @@ describe Milestone, 'Milestoneish' do
end
end
+ describe '#remaining_days' do
+ it 'shows 0 if no due date' do
+ milestone = build_stubbed(:milestone)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows 0 if expired' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.ago)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows correct remaining days' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.from_now)
+
+ expect(milestone.remaining_days).to eq(2)
+ end
+ end
+
describe '#elapsed_days' do
it 'shows 0 if no start_date set' do
- milestone = build(:milestone)
+ milestone = build_stubbed(:milestone)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows 0 if start_date is a future' do
- milestone = build(:milestone, start_date: Time.now + 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now + 2.days)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows correct amount of days' do
- milestone = build(:milestone, start_date: Time.now - 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now - 2.days)
expect(milestone.elapsed_days).to eq(2)
end
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
new file mode 100644
index 00000000000..255b584a85e
--- /dev/null
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -0,0 +1,204 @@
+require 'spec_helper'
+
+describe Issue, 'RelativePositioning' do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:new_issue) { create(:issue, project: project) }
+
+ before do
+ [issue, issue1].each do |issue|
+ issue.move_to_end && issue.save
+ end
+ end
+
+ describe '#max_relative_position' do
+ it 'returns maximum position' do
+ expect(issue.max_relative_position).to eq issue1.relative_position
+ end
+ end
+
+ describe '#prev_relative_position' do
+ it 'returns previous position if there is an issue above' do
+ expect(issue1.prev_relative_position).to eq issue.relative_position
+ end
+
+ it 'returns nil if there is no issue above' do
+ expect(issue.prev_relative_position).to eq nil
+ end
+ end
+
+ describe '#next_relative_position' do
+ it 'returns next position if there is an issue below' do
+ expect(issue.next_relative_position).to eq issue1.relative_position
+ end
+
+ it 'returns nil if there is no issue below' do
+ expect(issue1.next_relative_position).to eq nil
+ end
+ end
+
+ describe '#move_before' do
+ it 'moves issue before' do
+ [issue1, issue].each(&:move_to_end)
+
+ issue.move_before(issue1)
+
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+ end
+
+ describe '#move_after' do
+ it 'moves issue after' do
+ [issue, issue1].each(&:move_to_end)
+
+ issue.move_after(issue1)
+
+ expect(issue.relative_position).to be > issue1.relative_position
+ end
+ end
+
+ describe '#move_to_end' do
+ it 'moves issue to the end' do
+ new_issue.move_to_end
+
+ expect(new_issue.relative_position).to be > issue1.relative_position
+ end
+ end
+
+ describe '#shift_after?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position - 1)
+
+ expect(issue.shift_after?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position - 2)
+
+ expect(issue.shift_after?).to be_falsey
+ end
+ end
+
+ describe '#shift_before?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position + 1)
+
+ expect(issue.shift_before?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position + 2)
+
+ expect(issue.shift_before?).to be_falsey
+ end
+ end
+
+ describe '#move_between' do
+ it 'positions issue between two other' do
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(new_issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issue between on top' do
+ new_issue.move_between(nil, issue)
+
+ expect(new_issue.relative_position).to be < issue.relative_position
+ end
+
+ it 'positions issue between to end' do
+ new_issue.move_between(issue1, nil)
+
+ expect(new_issue.relative_position).to be > issue1.relative_position
+ end
+
+ it 'positions issues even when after and before positions are the same' do
+ issue1.update relative_position: issue.relative_position
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issues between other two if distance is 1' do
+ issue1.update relative_position: issue.relative_position + 1
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issue in the middle of other two if distance is big enough' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: 10000
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(8000)
+ end
+
+ it 'positions issue closer to the middle if we are at the very top' do
+ issue1.update relative_position: 6000
+
+ new_issue.move_between(nil, issue1)
+
+ expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue closer to the middle if we are at the very bottom' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: nil
+
+ new_issue.move_between(issue, nil)
+
+ expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue in the middle of other two if distance is not big enough' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 400
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(250)
+ end
+
+ it 'positions issue in the middle of other two is there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
+ end
+
+ it 'uses rebalancing if there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue1, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ expect(issue.reload.relative_position).not_to eq(100)
+ end
+
+ it 'positions issue right if we pass none-sequential parameters' do
+ issue.update relative_position: 99
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be(100)
+ end
+ end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index e008ec28fa4..677e60e1282 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -86,7 +86,7 @@ describe Group, 'Routable' do
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_path).to eq(group.path) }
- it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
+ it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
end
describe '#full_name' do
@@ -102,7 +102,7 @@ describe Project, 'Routable' do
describe '#full_path' do
let(:project) { build_stubbed(:empty_project) }
- it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" }
+ it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
end
describe '#full_name' do
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
new file mode 100644
index 00000000000..83187d732e4
--- /dev/null
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Uniquify, models: true do
+ let(:uniquify) { described_class.new }
+
+ describe "#string" do
+ it 'returns the given string if it does not exist' do
+ result = uniquify.string('test_string') { |s| false }
+
+ expect(result).to eq('test_string')
+ end
+
+ it 'returns the given string with a counter attached if the string exists' do
+ result = uniquify.string('test_string') { |s| s == 'test_string' }
+
+ expect(result).to eq('test_string1')
+ end
+
+ it 'increments the counter for each candidate string that also exists' do
+ result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' }
+
+ expect(result).to eq('test_string2')
+ end
+
+ it 'allows passing in a base function that defines the location of the counter' do
+ result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
+ s == 'test__string'
+ end
+
+ expect(result).to eq('test_1_string')
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index b9fe492fe2c..e6a826a9418 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
- sha = context.project.repository.commit_file(
+ sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 9a024d533a1..3a02ed81adb 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -18,7 +18,7 @@ describe 'CycleAnalytics#staging', feature: true do
start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
- end ]],
+ end]],
end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do
context.deploy_master
@@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
- sha = context.project.repository.commit_file(
+ sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index f0ed0c679d5..9f0e7fbbe26 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -76,7 +76,8 @@ describe Environment, models: true do
end
describe '#update_merge_request_metrics?' do
- { 'production' => true,
+ {
+ 'production' => true,
'production/eu' => true,
'production/www.gitlab.com' => true,
'productioneu' => false,
@@ -238,7 +239,7 @@ describe Environment, models: true do
describe '#actions_for' do
let(:deployment) { create(:deployment, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
- let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
+ let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
it 'returns a list of actions with matching environment' do
@@ -270,7 +271,11 @@ describe Environment, models: true do
context 'when the environment is unavailable' do
let(:project) { create(:kubernetes_project) }
- before { environment.stop }
+
+ before do
+ environment.stop
+ end
+
it { is_expected.to be_falsy }
end
end
@@ -280,20 +285,85 @@ describe Environment, models: true do
subject { environment.terminals }
context 'when the environment has terminals' do
- before { allow(environment).to receive(:has_terminals?).and_return(true) }
+ before do
+ allow(environment).to receive(:has_terminals?).and_return(true)
+ end
it 'returns the terminals from the deployment service' do
- expect(project.deployment_service).
- to receive(:terminals).with(environment).
- and_return(:fake_terminals)
+ expect(project.deployment_service)
+ .to receive(:terminals).with(environment)
+ .and_return(:fake_terminals)
is_expected.to eq(:fake_terminals)
end
end
context 'when the environment does not have terminals' do
- before { allow(environment).to receive(:has_terminals?).and_return(false) }
- it { is_expected.to eq(nil) }
+ before do
+ allow(environment).to receive(:has_terminals?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_metrics?' do
+ subject { environment.has_metrics? }
+
+ context 'when the enviroment is available' do
+ context 'with a deployment service' do
+ let(:project) { create(:prometheus_project) }
+
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'without a monitoring service' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when the environment is unavailable' do
+ let(:project) { create(:prometheus_project) }
+
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#metrics' do
+ let(:project) { create(:prometheus_project) }
+ subject { environment.metrics }
+
+ context 'when the environment has metrics' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(true)
+ end
+
+ it 'returns the metrics from the deployment service' do
+ expect(project.monitoring_service)
+ .to receive(:metrics).with(environment)
+ .and_return(:fake_metrics)
+
+ is_expected.to eq(:fake_metrics)
+ end
+ end
+
+ context 'when the environment does not have metrics' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
end
end
@@ -311,7 +381,7 @@ describe Environment, models: true do
end
describe '#generate_slug' do
- SUFFIX = "-[a-z0-9]{6}"
+ SUFFIX = "-[a-z0-9]{6}".freeze
{
"staging-12345678901234567" => "staging-123456789" + SUFFIX,
"9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 2debe1289a3..cd50bda8996 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -42,4 +42,12 @@ describe ExternalIssue, models: true do
expect(issue.project_id).to eq(project.id)
end
end
+
+ describe '#hash' do
+ it 'returns the hash of its [class, to_s] pair' do
+ issue_2 = described_class.new(issue.to_s, project)
+
+ expect(issue.hash).to eq(issue_2.hash)
+ end
+ end
end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index cacbab8bcb1..55b87d1c48a 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do
end
end
+ describe '.states_count' do
+ context 'when the projects have milestones' do
+ before do
+ create(:closed_milestone, title: 'Active Group Milestone', project: project3)
+ create(:active_milestone, title: 'Active Group Milestone', project: project1)
+ create(:active_milestone, title: 'Active Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
+ end
+
+ it 'returns the quantity of global milestones in each possible state' do
+ expected_count = { opened: 1, closed: 2, all: 2 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+
+ context 'when the projects do not have milestones' do
+ before do
+ project1
+ end
+
+ it 'returns 0 as the quantity of global milestones in each state' do
+ expected_count = { opened: 0, closed: 0, all: 0 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+ end
+
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
@@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do
expect(global_milestone.safe_title).to eq('git-test')
end
end
+
+ describe '#state' do
+ context 'when at least one milestone is active' do
+ it 'returns active' do
+ title = 'Active Group Milestone'
+ milestones = [
+ create(:active_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('active')
+ end
+ end
+
+ context 'when all milestones are closed' do
+ it 'returns closed' do
+ title = 'Closed Group Milestone'
+ milestones = [
+ create(:closed_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('closed')
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index a4e6eb4e3a6..5d87938235a 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -13,6 +13,8 @@ describe Group, models: true do
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_one(:chat_team) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bba9058f394..73977d031f9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,6 +22,45 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
+ describe '#order_by_position_and_priority' do
+ let(:project) { create :empty_project }
+ let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+ let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+ let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+ let!(:issue3) { create(:issue, project: project, relative_position: 100) }
+ let!(:issue4) { create(:issue, project: project, relative_position: 200) }
+
+ it 'returns ordered list' do
+ expect(project.issues.order_by_position_and_priority).
+ to match [issue3, issue4, issue1, issue2]
+ end
+ end
+
+ describe '#closed_at' do
+ after do
+ Timecop.return
+ end
+
+ let!(:now) { Timecop.freeze(Time.now) }
+
+ it 'sets closed_at to Time.now when issue is closed' do
+ issue = create(:issue, state: 'opened')
+
+ issue.close
+
+ expect(issue.closed_at).to eq(now)
+ end
+
+ it 'sets closed_at to nil when issue is reopened' do
+ issue = create(:issue, state: 'closed')
+
+ issue.reopen
+
+ expect(issue.closed_at).to be_nil
+ end
+ end
+
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
@@ -620,4 +659,15 @@ describe Issue, models: true do
end
end
end
+
+ describe '#hook_attrs' do
+ let(:attrs_hash) { subject.hook_attrs }
+
+ it 'includes time tracking attrs' do
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ expect(attrs_hash).to include(:human_total_time_spent)
+ expect(attrs_hash).to include('time_estimate')
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index e4be0aba7a6..87ea2e70680 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -89,8 +89,8 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [ @user_1, :developer ]
- @project_2.team << [ @user_2, :reporter ]
+ @project_1.team << [@user_1, :developer]
+ @project_2.team << [@user_2, :reporter]
@status = @project_2.team.import(@project_1)
end
@@ -137,8 +137,8 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [ @user_1, :developer]
- @project_2.team << [ @user_2, :reporter]
+ @project_1.team << [@user_1, :developer]
+ @project_2.team << [@user_2, :reporter]
ProjectMember.truncate_teams([@project_1.id, @project_2.id])
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 6d599e148a2..0a10ee01506 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -109,7 +109,7 @@ describe MergeRequestDiff, models: true do
{ id: 'sha2' }
]
- expect(subject.commits_sha).to eq(['sha1', 'sha2'])
+ expect(subject.commits_sha).to eq(%w(sha1 sha2))
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a01741a9971..24e7c1b17d9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -37,12 +37,12 @@ describe MergeRequest, models: true do
end
it "is invalid without merge user" do
- subject.merge_when_build_succeeds = true
+ subject.merge_when_pipeline_succeeds = true
expect(subject).not_to be_valid
end
it "is valid with merge user" do
- subject.merge_when_build_succeeds = true
+ subject.merge_when_pipeline_succeeds = true
subject.merge_user = build(:user)
expect(subject).to be_valid
@@ -55,7 +55,7 @@ describe MergeRequest, models: true do
it { is_expected.to respond_to(:can_be_merged?) }
it { is_expected.to respond_to(:cannot_be_merged?) }
it { is_expected.to respond_to(:merge_params) }
- it { is_expected.to respond_to(:merge_when_build_succeeds) }
+ it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
end
describe '.in_projects' do
@@ -209,6 +209,50 @@ describe MergeRequest, models: true do
end
end
+ describe '#diff_size' do
+ let(:merge_request) do
+ build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
+ end
+
+ context 'when there are MR diffs' do
+ before do
+ merge_request.save
+ end
+
+ it 'returns the correct count' do
+ expect(merge_request.diff_size).to eq(105)
+ end
+
+ it 'does not perform highlighting' do
+ expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+ merge_request.diff_size
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ before do
+ merge_request.compare = CompareService.new(
+ merge_request.source_project,
+ merge_request.source_branch
+ ).execute(
+ merge_request.target_project,
+ merge_request.target_branch
+ )
+ end
+
+ it 'returns the correct count' do
+ expect(merge_request.diff_size).to eq(105)
+ end
+
+ it 'does not perform highlighting' do
+ expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+ merge_request.diff_size
+ end
+ end
+ end
+
describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
@@ -302,6 +346,23 @@ describe MergeRequest, models: true do
expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
end
+
+ context 'when the project has an external issue tracker' do
+ before do
+ subject.project.team << [subject.author, :developer]
+ commit = double(:commit, safe_message: 'Fixes TEST-3')
+
+ create(:jira_service, project: subject.project)
+
+ allow(subject).to receive(:commits).and_return([commit])
+ allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
+ allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
+ end
+
+ it 'detects issues mentioned in description but not closed' do
+ expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
+ end
+ end
end
describe "#work_in_progress?" do
@@ -464,24 +525,24 @@ describe MergeRequest, models: true do
end
end
- describe "#reset_merge_when_build_succeeds" do
+ describe "#reset_merge_when_pipeline_succeeds" do
let(:merge_if_green) do
- create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
+ create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
end
it "sets the item to false" do
- merge_if_green.reset_merge_when_build_succeeds
+ merge_if_green.reset_merge_when_pipeline_succeeds
merge_if_green.reload
- expect(merge_if_green.merge_when_build_succeeds).to be_falsey
+ expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
expect(merge_if_green.merge_params["commit_message"]).to be_nil
end
end
describe "#hook_attrs" do
- let(:attrs_hash) { subject.hook_attrs.to_h }
+ let(:attrs_hash) { subject.hook_attrs }
[:source, :target].each do |key|
describe "#{key} key" do
@@ -497,6 +558,10 @@ describe MergeRequest, models: true do
expect(attrs_hash).to include(:target)
expect(attrs_hash).to include(:last_commit)
expect(attrs_hash).to include(:work_in_progress)
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ expect(attrs_hash).to include(:human_total_time_spent)
+ expect(attrs_hash).to include('time_estimate')
end
end
@@ -768,7 +833,7 @@ describe MergeRequest, models: true do
end
describe '#check_if_can_be_merged' do
- let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) }
subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
@@ -789,12 +854,6 @@ describe MergeRequest, models: true do
it 'becomes unmergeable' do
expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
end
-
- it 'creates Todo on unmergeability' do
- expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject)
-
- subject.check_if_can_be_merged
- end
end
context 'when it has conflicts' do
@@ -889,7 +948,7 @@ describe MergeRequest, models: true do
end
describe '#mergeable_ci_state?' do
- let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) }
let(:pipeline) { create(:ci_empty_pipeline) }
subject { build(:merge_request, target_project: project) }
@@ -932,7 +991,7 @@ describe MergeRequest, models: true do
end
context 'when merges are not restricted to green builds' do
- subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }
+ subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_pipeline_succeeds: false)) }
context 'and a failed pipeline is associated' do
before do
@@ -1537,7 +1596,7 @@ describe MergeRequest, models: true do
status: status)
end
- let(:project) { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
let(:developer) { create(:user) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index aeb4eeb0b55..d22447c602f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,20 @@ describe Namespace, models: true do
expect(nested).not_to be_valid
expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting')
end
+
+ describe 'reserved path validation' do
+ context 'nested group' do
+ let(:group) { build(:group, :nested, path: 'tree') }
+
+ it { expect(group).not_to be_valid }
+ end
+
+ context 'top-level group' do
+ let(:group) { build(:group, path: 'tree') }
+
+ it { expect(group).to be_valid }
+ end
+ end
end
describe "Respond to" do
@@ -36,7 +50,7 @@ describe Namespace, models: true do
end
describe '#to_param' do
- it { expect(namespace.to_param).to eq(namespace.path) }
+ it { expect(namespace.to_param).to eq(namespace.full_path) }
end
describe '#human_name' do
@@ -153,7 +167,7 @@ describe Namespace, models: true do
describe :rm_dir do
let!(:project) { create(:empty_project, namespace: namespace) }
- let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
+ let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
it "removes its dirs when deleted" do
namespace.destroy
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1cde9e04951..33536487c41 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -387,4 +387,16 @@ describe Note, models: true do
end
end
end
+
+ describe 'expiring ETag cache' do
+ let(:note) { build(:note_on_issue) }
+
+ it "expires cache for note's issue when note is saved" do
+ expect_any_instance_of(Gitlab::EtagCaching::Store)
+ .to receive(:touch)
+ .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+
+ note.save!
+ end
+ end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 46eb71cef14..823623d96fa 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -1,15 +1,61 @@
require 'spec_helper'
describe PersonalAccessToken, models: true do
- describe ".generate" do
- it "generates a random token" do
- personal_access_token = PersonalAccessToken.generate({})
- expect(personal_access_token.token).to be_present
+ describe '.build' do
+ let(:personal_access_token) { build(:personal_access_token) }
+ let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) }
+
+ it 'is a valid personal access token' do
+ expect(personal_access_token).to be_valid
+ end
+
+ it 'ensures that the token is generated' do
+ invalid_personal_access_token.save!
+
+ expect(invalid_personal_access_token).to be_valid
+ expect(invalid_personal_access_token.token).not_to be_nil
end
+ end
+
+ describe ".active?" do
+ let(:active_personal_access_token) { build(:personal_access_token) }
+ let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) }
+ let(:expired_personal_access_token) { build(:personal_access_token, :expired) }
+
+ it "returns false if the personal_access_token is revoked" do
+ expect(revoked_personal_access_token).not_to be_active
+ end
+
+ it "returns false if the personal_access_token is expired" do
+ expect(expired_personal_access_token).not_to be_active
+ end
+
+ it "returns true if the personal_access_token is not revoked and not expired" do
+ expect(active_personal_access_token).to be_active
+ end
+ end
+
+ context "validations" do
+ let(:personal_access_token) { build(:personal_access_token) }
+
+ it "requires at least one scope" do
+ personal_access_token.scopes = []
+
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:scopes].first).to eq "can't be blank"
+ end
+
+ it "allows creating a token with API scopes" do
+ personal_access_token.scopes = [:api, :read_user]
+
+ expect(personal_access_token).to be_valid
+ end
+
+ it "rejects creating a token with non-API scopes" do
+ personal_access_token.scopes = [:openid, :api]
- it "doesn't save the record" do
- personal_access_token = PersonalAccessToken.generate({})
- expect(personal_access_token).not_to be_persisted
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
end
end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 59a4ae1b799..9b711bfc007 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -7,12 +7,27 @@ describe ProjectGroupLink do
end
describe "Validation" do
- let!(:project_group_link) { create(:project_group_link) }
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:project) { create(:project, group: group) }
+ let!(:project_group_link) { create(:project_group_link, project: project) }
it { should validate_presence_of(:project_id) }
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
+
+ it "doesn't allow a project to be shared with the group it is in" do
+ project_group_link.group = group
+
+ expect(project_group_link).not_to be_valid
+ end
+
+ it "doesn't allow a project to be shared with an ancestor of the group it is in" do
+ project_group_link.group = parent_group
+
+ expect(project_group_link).not_to be_valid
+ end
end
describe "destroying a record", truncate: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 497a626a418..4014d6129ee 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -181,7 +181,7 @@ describe BambooService, models: true, caching: true do
end
it 'sets commit status to "pending" when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ stub_request(body: %q({"results":{"results":{"size":"0"}}}))
is_expected.to eq('pending')
end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index dbd23ff5491..05b602d8106 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -92,7 +92,7 @@ describe BuildkiteService, models: true, caching: true do
end
it 'passes through build status untouched when status is 200' do
- stub_request(body: %Q({"status":"Great Success"}))
+ stub_request(body: %q({"status":"Great Success"}))
is_expected.to eq('Great Success')
end
@@ -101,7 +101,7 @@ describe BuildkiteService, models: true, caching: true do
end
def stub_request(status: 200, body: nil)
- body ||= %Q({"status":"success"})
+ body ||= %q({"status":"success"})
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
WebMock.stub_request(:get, buildkite_full_url).to_return(
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e2563..00000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
- let(:data) do
- Gitlab::DataBuilder::Build.build(create(:ci_build))
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before { subject.active = true }
-
- it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before { subject.add_pusher = true }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- context 'when service is inactive' do
- before { subject.active = false }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- describe '#test_data' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
- let(:user) { create(:user) }
-
- before { project.team << [user, :developer] }
-
- it 'builds test data' do
- data = subject.test_data(project)
-
- expect(data[:object_kind]).to eq("build")
- end
- end
-
- describe '#test' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- subject.recipients = 'test@gitlab.com'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
-
- context 'notify only failed builds is true' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- data[:build_status] = "success"
- subject.recipients = 'test@gitlab.com'
-
- expect(subject).not_to receive(:notify_only_broken_builds)
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
- end
- end
-
- describe '#execute' do
- it 'sends email' do
- subject.recipients = 'test@gitlab.com'
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with succeeded build and notify_only_broken_builds on' do
- expect(subject).to receive(:notify_only_broken_builds).and_return(true)
- data[:build_status] = 'success'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with failed build and build_allow_failure is true' do
- data[:build_status] = 'failed'
- data[:build_allow_failure] = true
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with unknown build status' do
- data[:build_status] = 'foo'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email when recipients list is empty' do
- subject.recipients = ' ,, '
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
- end
-end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0..00000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
- subject { described_class.new(args) }
-
- let(:args) do
- {
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- ref: 'develop',
- tag: false,
-
- project_name: 'project_name',
- project_url: 'http://example.gitlab.com',
- build_id: 1,
- build_name: build_name,
- build_stage: stage,
-
- commit: {
- status: status,
- author_name: 'hacker',
- author_url: 'http://example.gitlab.com/hacker',
- duration: duration,
- },
- }
- end
-
- let(:message) { build_message }
- let(:stage) { 'test' }
- let(:status) { 'success' }
- let(:build_name) { 'rspec' }
- let(:duration) { 10 }
-
- context 'build succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'build failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- it 'returns a message with information on build' do
- expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
- end
-
- it 'returns a message with stage name' do
- expect(subject.fallback).to include("of stage #{stage}")
- end
-
- it 'returns a message with link to author' do
- expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
- end
-
- def build_message(status_text = status, stage_text = stage, build_text = build_name)
- "<http://example.gitlab.com|project_name>:" \
- " Commit <http://example.gitlab.com/commit/" \
- "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
- " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
- " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index f9307d6de7b..044737c6026 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -28,7 +28,7 @@ describe DroneCiService, models: true, caching: true do
shared_context :drone_ci_service do
let(:drone) { DroneCiService.new }
let(:project) { create(:project, :repository, name: 'project') }
- let(:path) { "#{project.namespace.path}/#{project.path}" }
+ let(:path) { project.full_path }
let(:drone_url) { 'http://drone.example.com' }
let(:sha) { '2ab7834c' }
let(:branch) { 'dev' }
@@ -50,7 +50,7 @@ describe DroneCiService, models: true, caching: true do
end
def stub_request(status: 200, body: nil)
- body ||= %Q({"status":"success"})
+ body ||= %q({"status":"success"})
WebMock.stub_request(:get, commit_status_path).to_return(
status: status,
@@ -95,12 +95,12 @@ describe DroneCiService, models: true, caching: true do
is_expected.to eq(:error)
end
- { "killed" => :canceled,
+ {
+ "killed" => :canceled,
"failure" => :failed,
"error" => :failed,
- "success" => "success",
+ "success" => "success"
}.each do |drone_status, our_status|
-
it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
stub_request(body: %Q({"status":"#{drone_status}"}))
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1..1200ae7eb22 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -280,13 +280,14 @@ describe HipchatService, models: true do
end
end
- context 'build events' do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'for failed' do
- before { build.drop }
+ before do
+ pipeline.drop
+ end
it "calls Hipchat API" do
hipchat.execute(data)
@@ -295,35 +296,36 @@ describe HipchatService, models: true do
end
it "creates a build message" do
- message = hipchat.send(:create_build_message, data)
+ message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
project_name = project.name_with_namespace.gsub(/\s/, '')
- sha = data[:sha]
- ref = data[:ref]
- ref_type = data[:tag] ? 'tag' : 'branch'
- duration = data[:commit][:duration]
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
- "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
"of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
- "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+ "by #{user_name} failed in #{duration} second(s)")
end
end
context 'for succeeded' do
before do
- build.success
+ pipeline.succeed
end
it "calls Hipchat API" do
- hipchat.notify_only_broken_builds = false
+ hipchat.notify_only_broken_pipelines = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "notifies only broken" do
- hipchat.notify_only_broken_builds = true
+ hipchat.notify_only_broken_pipelines = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
end
@@ -349,17 +351,19 @@ describe HipchatService, models: true do
context 'with a successful build' do
it 'uses the green color' do
- build_data = { object_kind: 'build', commit: { status: 'success' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
end
end
context 'with a failed build' do
it 'uses the red color' do
- build_data = { object_kind: 'build', commit: { status: 'failed' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index b9fb6f3f6f4..d5a16226d9d 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -59,8 +59,8 @@ describe IrkerService, models: true do
conn = @irker_server.accept
conn.readlines.each do |line|
- msg = JSON.load(line.chomp("\n"))
- expect(msg.keys).to match_array(['to', 'privmsg'])
+ msg = JSON.parse(line.chomp("\n"))
+ expect(msg.keys).to match_array(%w(to privmsg))
expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
"irc://test.net/#test"])
end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
new file mode 100644
index 00000000000..fbe6f344a98
--- /dev/null
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe IssueTrackerService, models: true do
+ describe 'Validations' do
+ let(:project) { create :project }
+
+ describe 'only one issue tracker per project' do
+ let(:service) { RedmineService.new(project: project, active: true) }
+
+ before do
+ create(:service, project: project, active: true, category: 'issue_tracker')
+ end
+
+ context 'when service is changed manually by user' do
+ it 'executes the validation' do
+ valid = service.valid?(:manual_change)
+
+ expect(valid).to be_falsey
+ expect(service.errors[:base]).to include(
+ 'Another issue tracker is already in use. Only one issue tracker service can be active at a time'
+ )
+ end
+ end
+
+ context 'when service is changed internally' do
+ it 'does not execute the validation' do
+ expect(service.valid?).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 9052479d35e..bf7950ef1c9 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -74,8 +74,10 @@ describe KubernetesService, models: true, caching: true do
describe '#initialize_properties' do
context 'with a project' do
- it 'defaults to the project name' do
- expect(described_class.new(project: project).namespace).to eq(project.name)
+ let(:namespace_name) { "#{project.path}-#{project.id}" }
+
+ it 'defaults to the project name with ID' do
+ expect(described_class.new(project: project).namespace).to eq(namespace_name)
end
end
@@ -163,6 +165,12 @@ describe KubernetesService, models: true, caching: true do
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
)
end
+
+ it 'sets KUBE_CA_PEM_FILE' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
+ )
+ end
end
describe '#terminals' do
@@ -171,7 +179,7 @@ describe KubernetesService, models: true, caching: true do
context 'with invalid pods' do
it 'returns no terminals' do
- stub_reactive_cache(service, pods: [ { "bad" => "pod" } ])
+ stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
is_expected.to be_empty
end
@@ -184,7 +192,7 @@ describe KubernetesService, models: true, caching: true do
before do
stub_reactive_cache(
service,
- pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
)
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 98f3d420c8a..f9531be5d25 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -36,7 +36,8 @@ describe MattermostSlashCommandsService, :models do
description: "Perform common operations on: #{project.name_with_namespace}",
display_name: "GitLab / #{project.name_with_namespace}",
method: 'P',
- username: 'GitLab' }.to_json).
+ username: 'GitLab'
+ }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
@@ -91,7 +92,7 @@ describe MattermostSlashCommandsService, :models do
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
- body: ['list'].to_json
+ body: { 'list' => true }.to_json
)
end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
new file mode 100644
index 00000000000..d15079b686b
--- /dev/null
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe PrometheusService, models: true, caching: true do
+ include PrometheusHelpers
+ include ReactiveCachingHelpers
+
+ let(:project) { create(:prometheus_project) }
+ let(:service) { project.prometheus_service }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:api_url) }
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:api_url) }
+ end
+ end
+
+ describe '#test' do
+ let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) }
+
+ context 'success' do
+ it 'reads the discovery endpoint' do
+ expect(service.test[:success]).to be_truthy
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'failure' do
+ let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), status: 404) }
+
+ it 'fails to read the discovery endpoint' do
+ expect(service.test[:success]).to be_falsy
+ expect(req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#metrics' do
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ subject { service.metrics(environment) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ before do
+ stub_reactive_cache(service, prometheus_data, 'env-slug')
+ end
+
+ it 'returns reactive data' do
+ is_expected.to eq(prometheus_data)
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ subject do
+ service.calculate_reactive_cache(environment.slug)
+ end
+
+ context 'when service is inactive' do
+ before do
+ service.active = false
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when Prometheus responds with valid data' do
+ before do
+ stub_all_prometheus_requests(environment.slug)
+ end
+
+ it { expect(subject.to_json).to eq(prometheus_data.to_json) }
+ end
+
+ [404, 500].each do |status|
+ context "when Prometheus responds with #{status}" do
+ before do
+ stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
+ end
+
+ it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index a1edd083aa1..77b18e1c7d0 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -143,7 +143,7 @@ describe TeamcityService, models: true, caching: true do
end
it 'returns a build URL when teamcity_url has no trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ stub_request(body: %q({"build":{"id":"666"}}))
is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
@@ -152,7 +152,7 @@ describe TeamcityService, models: true, caching: true do
let(:teamcity_url) { 'http://gitlab.com/teamcity/' }
it 'returns a build URL' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ stub_request(body: %q({"build":{"id":"666"}}))
is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 77f2ff3d17b..aefbedf0b93 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,8 +29,7 @@ describe Project, models: true do
it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
- it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
- it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) }
it { is_expected.to have_one(:irker_service).dependent(:destroy) }
it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
@@ -71,6 +70,7 @@ describe Project, models: true do
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
context 'after initialized' do
it "has a project_feature" do
@@ -178,7 +178,7 @@ describe Project, models: true do
let(:project2) { build(:empty_project, repository_storage: 'missing') }
before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -218,6 +218,20 @@ describe Project, models: true do
expect(project2.import_data).to be_nil
end
+ it "does not allow blocked import_url localhost" do
+ project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
+ it "does not allow blocked import_url port" do
+ project2 = build(:empty_project, import_url: 'http://github.com:25/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
describe 'project pending deletion' do
let!(:project_pending_deletion) do
create(:empty_project,
@@ -380,7 +394,7 @@ describe Project, models: true do
before do
FileUtils.mkdir('tmp/tests/custom_repositories')
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -402,7 +416,7 @@ describe Project, models: true do
let(:project) { create(:empty_project, path: "somewhere") }
it 'returns the full web URL for this repo' do
- expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
+ expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
end
end
@@ -803,7 +817,7 @@ describe Project, models: true do
end
let(:avatar_path) do
- "/#{project.namespace.name}/#{project.path}/avatar"
+ "/#{project.full_path}/avatar"
end
it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
@@ -946,8 +960,8 @@ describe Project, models: true do
before do
storages = {
- 'default' => 'tmp/tests/repositories',
- 'picked' => 'tmp/tests/repositories',
+ 'default' => { 'path' => 'tmp/tests/repositories' },
+ 'picked' => { 'path' => 'tmp/tests/repositories' },
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -1148,16 +1162,14 @@ describe Project, models: true do
end
it 'renames a repository' do
- ns = project.namespace_dir
-
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}").
+ with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
and_return(true)
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
+ with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki").
and_return(true)
expect_any_instance_of(SystemHooksService).
@@ -1166,7 +1178,7 @@ describe Project, models: true do
expect_any_instance_of(Gitlab::UploadsTransfer).
to receive(:rename_project).
- with('foo', project.path, ns)
+ with('foo', project.path, project.namespace.full_path)
expect(project).to receive(:expire_caches_before_rename)
@@ -1513,7 +1525,7 @@ describe Project, models: true do
it 'schedules a RepositoryForkWorker job' do
expect(RepositoryForkWorker).to receive(:perform_async).
with(project.id, forked_from_project.repository_storage_path,
- forked_from_project.path_with_namespace, project.namespace.path)
+ forked_from_project.path_with_namespace, project.namespace.full_path)
project.add_import_job
end
@@ -1727,7 +1739,7 @@ describe Project, models: true do
describe 'inside_path' do
let!(:project1) { create(:empty_project) }
let!(:project2) { create(:empty_project) }
- let!(:path) { project1.namespace.path }
+ let!(:path) { project1.namespace.full_path }
it { expect(Project.inside_path(path)).to eq([project1]) }
end
@@ -1742,7 +1754,7 @@ describe Project, models: true do
end
before do
- project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master')
end
context 'when there is a .gitlab/route-map.yml at the commit' do
@@ -1871,4 +1883,34 @@ describe Project, models: true do
end
end
end
+
+ describe '#http_url_to_repo' do
+ let(:project) { create :empty_project }
+
+ context 'when no user is given' do
+ it 'returns the url to the repo without a username' do
+ expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(project.http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when user is given' do
+ it 'returns the url to the repo with the username' do
+ user = build_stubbed(:user)
+
+ expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+ end
+ end
+ end
+
+ describe '#pipeline_status' do
+ let(:project) { create(:project) }
+ it 'builds a pipeline status' do
+ expect(project.pipeline_status).to be_a(Ci::PipelineStatus)
+ end
+
+ it 'hase a loaded pipeline status' do
+ expect(project.pipeline_status).to be_loaded
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 58b57bd4fef..b5b9cd024b0 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -35,10 +35,23 @@ describe ProjectWiki, models: true do
end
describe "#http_url_to_repo" do
- it "provides the full http url to the repo" do
- gitlab_url = Gitlab.config.gitlab.url
- repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git"
- expect(subject.http_url_to_repo).to eq(repo_http_url)
+ let(:project) { create :empty_project }
+
+ context 'when no user is given' do
+ it 'returns the url to the repo without a username' do
+ expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git"
+
+ expect(project_wiki.http_url_to_repo).to eq(expected_url)
+ expect(project_wiki.http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when user is given' do
+ it 'returns the url to the repo with the username' do
+ user = build_stubbed(:user)
+
+ expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 838fd3754b2..274e4f00a0a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -291,10 +291,10 @@ describe Repository, models: true do
end
end
- describe "#commit_dir" do
+ describe "#create_dir" do
it "commits a change that creates a new directory" do
expect do
- repository.commit_dir(user, 'newdir',
+ repository.create_dir(user, 'newdir',
message: 'Create newdir', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
@@ -307,7 +307,7 @@ describe Repository, models: true do
it "creates a fork and commit to the forked project" do
expect do
- repository.commit_dir(user, 'newdir',
+ repository.create_dir(user, 'newdir',
message: 'Create newdir', branch_name: 'patch',
start_branch_name: 'master', start_project: forked_project)
end.to change { repository.commits('master').count }.by(0)
@@ -323,7 +323,7 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_dir(user, 'newdir',
+ repository.create_dir(user, 'newdir',
message: 'Add newdir',
branch_name: 'master',
author_email: author_email, author_name: author_name)
@@ -337,25 +337,23 @@ describe Repository, models: true do
end
end
- describe "#commit_file" do
- it 'commits change to a file successfully' do
+ describe "#create_file" do
+ it 'commits new file successfully' do
expect do
- repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- message: 'Updates file content',
- branch_name: 'master',
- update: true)
+ repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
+ message: 'Create changelog',
+ branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
- blob = repository.blob_at('master', 'CHANGELOG')
+ blob = repository.blob_at('master', 'NEWCHANGELOG')
expect(blob.data).to eq('Changelog!')
end
it 'respects the autocrlf setting' do
- repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld",
+ repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
message: 'Add hello world',
- branch_name: 'master',
- update: true)
+ branch_name: 'master')
blob = repository.blob_at('master', 'hello.txt')
@@ -365,10 +363,9 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_file(user, 'README', 'README!',
+ repository.create_file(user, 'NEWREADME', 'README!',
message: 'Add README',
branch_name: 'master',
- update: true,
author_email: author_email,
author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
@@ -382,6 +379,18 @@ describe Repository, models: true do
end
describe "#update_file" do
+ it 'updates file successfully' do
+ expect do
+ repository.update_file(user, 'CHANGELOG', 'Changelog!',
+ message: 'Update changelog',
+ branch_name: 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ blob = repository.blob_at('master', 'CHANGELOG')
+
+ expect(blob.data).to eq('Changelog!')
+ end
+
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -398,9 +407,6 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, 'README', 'README!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
repository.update_file(user, 'README', 'Updated README!',
branch_name: 'master',
@@ -418,13 +424,10 @@ describe Repository, models: true do
end
end
- describe "#remove_file" do
+ describe "#delete_file" do
it 'removes file successfully' do
- repository.commit_file(user, 'README', 'README!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
- repository.remove_file(user, 'README',
+ repository.delete_file(user, 'README',
message: 'Remove README', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
@@ -433,11 +436,8 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, 'README', 'README!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
- repository.remove_file(user, 'README',
+ repository.delete_file(user, 'README',
message: 'Remove README', branch_name: 'master',
author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
@@ -587,14 +587,14 @@ describe Repository, models: true do
describe "#license_blob", caching: true do
before do
- repository.remove_file(
+ repository.delete_file(
user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
end
it 'handles when HEAD points to non-existent ref' do
- repository.commit_file(
+ repository.create_file(
user, 'LICENSE', 'Copyright!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ message: 'Add LICENSE', branch_name: 'master')
allow(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
@@ -603,27 +603,27 @@ describe Repository, models: true do
end
it 'looks in the root_ref only' do
- repository.remove_file(user, 'LICENSE',
+ repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'markdown')
- repository.commit_file(user, 'LICENSE',
+ repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
- message: 'Add LICENSE', branch_name: 'markdown', update: false)
+ message: 'Add LICENSE', branch_name: 'markdown')
expect(repository.license_blob).to be_nil
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_blob.name).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
it "detects '#{filename}'" do
- repository.commit_file(user, filename,
+ repository.create_file(user, filename,
Licensee::License.new('mit').content,
- message: "Add #{filename}", branch_name: 'master', update: false)
+ message: "Add #{filename}", branch_name: 'master')
expect(repository.license_blob.name).to eq(filename)
end
@@ -632,7 +632,7 @@ describe Repository, models: true do
describe '#license_key', caching: true do
before do
- repository.remove_file(user, 'LICENSE',
+ repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
@@ -647,16 +647,16 @@ describe Repository, models: true do
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to be_nil
end
it 'returns the license key' do
- repository.commit_file(user, 'LICENSE',
+ repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq('mit')
end
@@ -913,10 +913,9 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_emptiness_caches)
expect(empty_repository).to receive(:expire_branches_cache)
- empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+ empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
message: 'Updates file content',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
end
end
end
@@ -1043,7 +1042,7 @@ describe Repository, models: true do
it 'expires the cache for all branches' do
expect(cache).to receive(:expire).
- at_least(repository.branches.length).
+ at_least(repository.branches.length * 2).
times
repository.expire_branch_cache
@@ -1051,14 +1050,14 @@ describe Repository, models: true do
it 'expires the cache for all branches when the root branch is given' do
expect(cache).to receive(:expire).
- at_least(repository.branches.length).
+ at_least(repository.branches.length * 2).
times
repository.expire_branch_cache(repository.root_ref)
end
it 'expires the cache for a specific branch' do
- expect(cache).to receive(:expire).once
+ expect(cache).to receive(:expire).twice
repository.expire_branch_cache('foo')
end
@@ -1113,16 +1112,16 @@ describe Repository, models: true do
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
context 'when there is a conflict' do
- it 'aborts the operation' do
- expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
+ it 'raises an error' do
+ expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/)
end
end
context 'when commit was already reverted' do
- it 'aborts the operation' do
+ it 'raises an error' do
repository.revert(user, update_image_commit, 'master')
- expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
+ expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/)
end
end
@@ -1149,16 +1148,16 @@ describe Repository, models: true do
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
context 'when there is a conflict' do
- it 'aborts the operation' do
- expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
+ it 'raises an error' do
+ expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/)
end
end
context 'when commit was already cherry-picked' do
- it 'aborts the operation' do
+ it 'raises an error' do
repository.cherry_pick(user, pickable_commit, 'master')
- expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
+ expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/)
end
end
@@ -1382,13 +1381,13 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
- expect(repository.branch_count).to be_an_instance_of(Fixnum)
+ expect(repository.branch_count).to be_an(Integer)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
- expect(repository.tag_count).to be_an_instance_of(Fixnum)
+ expect(repository.tag_count).to be_an(Integer)
end
end
@@ -1738,7 +1737,30 @@ describe Repository, models: true do
context 'with an existing repository' do
it 'returns the commit count' do
- expect(repository.commit_count).to be_an_instance_of(Fixnum)
+ expect(repository.commit_count).to be_an(Integer)
+ end
+ end
+ end
+
+ describe '#commit_count_for_ref' do
+ let(:project) { create :empty_project }
+
+ context 'with a non-existing repository' do
+ it 'returns 0' do
+ expect(project.repository.commit_count_for_ref('master')).to eq(0)
+ end
+ end
+
+ context 'with empty repository' do
+ it 'returns 0' do
+ project.create_repository
+ expect(project.repository.commit_count_for_ref('master')).to eq(0)
+ end
+ end
+
+ context 'when searching for the root ref' do
+ it 'returns the same count as #commit_count' do
+ expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count)
end
end
end
@@ -1796,7 +1818,7 @@ describe Repository, models: true do
describe '#gitlab_ci_yml_for' do
before do
- repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false)
+ repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
end
context 'when there is a .gitlab-ci.yml at the commit' do
@@ -1814,7 +1836,7 @@ describe Repository, models: true do
describe '#route_map_for' do
before do
- repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master')
end
context 'when there is a .gitlab/route-map.yml at the commit' do
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 0b222022e62..bc8ae4ae5a8 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -43,14 +43,22 @@ describe Route, models: true do
end
context 'name update' do
- before { route.update_attributes(name: 'bar') }
-
it "updates children routes with new path" do
+ route.update_attributes(name: 'bar')
+
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
end
+
+ it 'handles a rename from nil' do
+ # Note: using `update_columns` to skip all validation and callbacks
+ route.update_columns(name: nil)
+
+ expect { route.update_attributes(name: 'bar') }
+ .to change { route.name }.from(nil).to('bar')
+ end
end
end
end
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
new file mode 100644
index 00000000000..4c832c87d6a
--- /dev/null
+++ b/spec/models/upload_spec.rb
@@ -0,0 +1,151 @@
+require 'rails_helper'
+
+describe Upload, type: :model do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:model) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:size) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_presence_of(:model) }
+ it { is_expected.to validate_presence_of(:uploader) }
+ end
+
+ describe 'callbacks' do
+ context 'for a file above the checksum threshold' do
+ it 'schedules checksum calculation' do
+ stub_const('UploadChecksumWorker', spy)
+
+ upload = described_class.create(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
+ model: build_stubbed(:user),
+ uploader: double('ExampleUploader')
+ )
+
+ expect(UploadChecksumWorker)
+ .to have_received(:perform_async).with(upload.id)
+ end
+ end
+
+ context 'for a file at or below the checksum threshold' do
+ it 'calculates checksum immediately before save' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD,
+ model: build_stubbed(:user),
+ uploader: double('ExampleUploader')
+ )
+
+ expect { upload.save }
+ .to change { upload.checksum }.from(nil)
+ .to(a_string_matching(/\A\h{64}\z/))
+ end
+ end
+ end
+
+ describe '.remove_path' do
+ it 'removes all records at the given path' do
+ described_class.create!(
+ size: File.size(__FILE__),
+ path: __FILE__,
+ model: build_stubbed(:user),
+ uploader: 'AvatarUploader'
+ )
+
+ expect { described_class.remove_path(__FILE__) }.
+ to change { described_class.count }.from(1).to(0)
+ end
+ end
+
+ describe '.record' do
+ let(:fake_uploader) do
+ double(
+ file: double(size: 12_345),
+ relative_path: 'foo/bar.jpg',
+ model: build_stubbed(:user),
+ class: 'AvatarUploader'
+ )
+ end
+
+ it 'removes existing paths before creation' do
+ expect(described_class).to receive(:remove_path)
+ .with(fake_uploader.relative_path)
+
+ described_class.record(fake_uploader)
+ end
+
+ it 'creates a new record and assigns size, path, model, and uploader' do
+ upload = described_class.record(fake_uploader)
+
+ aggregate_failures do
+ expect(upload).to be_persisted
+ expect(upload.size).to eq fake_uploader.file.size
+ expect(upload.path).to eq fake_uploader.relative_path
+ expect(upload.model_id).to eq fake_uploader.model.id
+ expect(upload.model_type).to eq fake_uploader.model.class.to_s
+ expect(upload.uploader).to eq fake_uploader.class
+ end
+ end
+ end
+
+ describe '#absolute_path' do
+ it 'returns the path directly when already absolute' do
+ path = '/path/to/namespace/project/secret/file.jpg'
+ upload = described_class.new(path: path)
+
+ expect(upload).not_to receive(:uploader_class)
+
+ expect(upload.absolute_path).to eq path
+ end
+
+ it "delegates to the uploader's absolute_path method" do
+ uploader = spy('FakeUploader')
+ upload = described_class.new(path: 'secret/file.jpg')
+ expect(upload).to receive(:uploader_class).and_return(uploader)
+
+ upload.absolute_path
+
+ expect(uploader).to have_received(:absolute_path).with(upload)
+ end
+ end
+
+ describe '#calculate_checksum' do
+ it 'calculates the SHA256 sum' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+ )
+ expected = Digest::SHA256.file(__FILE__).hexdigest
+
+ expect { upload.calculate_checksum }
+ .to change { upload.checksum }.from(nil).to(expected)
+ end
+
+ it 'returns nil for a non-existant file' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+ )
+
+ expect(upload).to receive(:exist?).and_return(false)
+
+ expect(upload.calculate_checksum).to be_nil
+ end
+ end
+
+ describe '#exist?' do
+ it 'returns true when the file exists' do
+ upload = described_class.new(path: __FILE__)
+
+ expect(upload).to exist
+ end
+
+ it 'returns false when the file does not exist' do
+ upload = described_class.new(path: "#{__FILE__}-nope")
+
+ expect(upload).not_to exist
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 584a4facd94..90378179e32 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,7 +22,7 @@ describe User, models: true do
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:recent_events).class_name('Event') }
- it { is_expected.to have_many(:issues).dependent(:destroy) }
+ it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
@@ -32,9 +32,11 @@ describe User, models: true do
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+ it { is_expected.to have_many(:triggers).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
@@ -693,12 +695,13 @@ describe User, models: true do
end
describe '.search_with_secondary_emails' do
- def search_with_secondary_emails(query)
- described_class.search_with_secondary_emails(query)
- end
+ delegate :search_with_secondary_emails, to: :described_class
- let!(:user) { create(:user) }
- let!(:email) { create(:email) }
+ let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
+ let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
+ let!(:email) do
+ create(:email, user: another_user, email: 'alias@example.com')
+ end
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
@@ -1415,7 +1418,7 @@ describe User, models: true do
it { expect(user.nested_groups).to eq([nested_group]) }
end
- describe '#nested_projects' do
+ describe '#nested_groups_projects' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
@@ -1430,7 +1433,7 @@ describe User, models: true do
other_project.add_developer(create(:user))
end
- it { expect(user.nested_projects).to eq([nested_project]) }
+ it { expect(user.nested_groups_projects).to eq([nested_project]) }
end
describe '#refresh_authorized_projects', redis: true do
@@ -1492,4 +1495,41 @@ describe User, models: true do
expect(user.admin).to be true
end
end
+
+ describe '.ghost' do
+ it "creates a ghost user if one isn't already present" do
+ ghost = User.ghost
+
+ expect(ghost).to be_ghost
+ expect(ghost).to be_persisted
+ end
+
+ it "does not create a second ghost user if one is already present" do
+ expect do
+ User.ghost
+ User.ghost
+ end.to change { User.count }.by(1)
+ expect(User.ghost).to eq(User.ghost)
+ end
+
+ context "when a regular user exists with the username 'ghost'" do
+ it "creates a ghost user with a non-conflicting username" do
+ create(:user, username: 'ghost')
+ ghost = User.ghost
+
+ expect(ghost).to be_persisted
+ expect(ghost.username).to eq('ghost1')
+ end
+ end
+
+ context "when a regular user exists with the email 'ghost@example.com'" do
+ it "creates a ghost user with a non-conflicting email" do
+ create(:user, email: 'ghost@example.com')
+ ghost = User.ghost
+
+ expect(ghost).to be_persisted
+ expect(ghost.email).to eq('ghost1@example.com')
+ end
+ end
+ end
end
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index 63acc0b68cd..02acdcb36df 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,17 +1,19 @@
require 'spec_helper'
describe BasePolicy, models: true do
- let(:build) { Ci::Build.new }
-
describe '.class_for' do
it 'detects policy class based on the subject ancestors' do
- expect(described_class.class_for(build)).to eq(Ci::BuildPolicy)
+ expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
end
it 'detects policy class for a presented subject' do
- presentee = Ci::BuildPresenter.new(build)
+ presentee = Ci::BuildPresenter.new(Ci::Build.new)
expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy)
end
+
+ it 'uses GlobalPolicy when :global is given' do
+ expect(described_class.class_for(:global)).to eq(GlobalPolicy)
+ end
end
end
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
new file mode 100644
index 00000000000..63ad5eb7322
--- /dev/null
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Ci::TriggerPolicy, :models do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+
+ let(:policies) do
+ described_class.abilities(user, trigger).to_set
+ end
+
+ shared_examples 'allows to admin and manage trigger' do
+ it 'does include ability to admin trigger' do
+ expect(policies).to include :admin_trigger
+ end
+
+ it 'does include ability to manage trigger' do
+ expect(policies).to include :manage_trigger
+ end
+ end
+
+ shared_examples 'allows to manage trigger' do
+ it 'does not include ability to admin trigger' do
+ expect(policies).not_to include :admin_trigger
+ end
+
+ it 'does include ability to manage trigger' do
+ expect(policies).to include :manage_trigger
+ end
+ end
+
+ shared_examples 'disallows to admin and manage trigger' do
+ it 'does not include ability to admin trigger' do
+ expect(policies).not_to include :admin_trigger
+ end
+
+ it 'does not include ability to manage trigger' do
+ expect(policies).not_to include :manage_trigger
+ end
+ end
+
+ describe '#rules' do
+ context 'when owner is undefined' do
+ let(:owner) { nil }
+
+ context 'when user is master of the project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it_behaves_like 'allows to admin and manage trigger'
+ end
+
+ context 'when user is developer of the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'disallows to admin and manage trigger'
+ end
+
+ context 'when user is not member of the project' do
+ it_behaves_like 'disallows to admin and manage trigger'
+ end
+ end
+
+ context 'when owner is an user' do
+ let(:owner) { user }
+
+ context 'when user is master of the project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it_behaves_like 'allows to admin and manage trigger'
+ end
+ end
+
+ context 'when owner is another user' do
+ let(:owner) { create(:user) }
+
+ context 'when user is master of the project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it_behaves_like 'allows to manage trigger'
+ end
+
+ context 'when user is developer of the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'disallows to admin and manage trigger'
+ end
+
+ context 'when user is not member of the project' do
+ it_behaves_like 'disallows to admin and manage trigger'
+ end
+ end
+ end
+end
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
new file mode 100644
index 00000000000..d5761390d39
--- /dev/null
+++ b/spec/policies/user_policy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe UserPolicy, models: true do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+
+ subject { described_class.abilities(current_user, user).to_set }
+
+ describe "reading a user's information" do
+ it { is_expected.to include(:read_user) }
+ end
+
+ describe "destroying a user" do
+ context "when a regular user tries to destroy another regular user" do
+ it { is_expected.not_to include(:destroy_user) }
+ end
+
+ context "when a regular user tries to destroy themselves" do
+ let(:current_user) { user }
+
+ it { is_expected.to include(:destroy_user) }
+ end
+
+ context "when an admin user tries to destroy a regular user" do
+ let(:current_user) { create(:user, :admin) }
+
+ it { is_expected.to include(:destroy_user) }
+ end
+
+ context "when an admin user tries to destroy a ghost user" do
+ let(:current_user) { create(:user, :admin) }
+ let(:user) { create(:user, :ghost) }
+
+ it { is_expected.not_to include(:destroy_user) }
+ end
+ end
+end
diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
new file mode 100644
index 00000000000..6443f86b6a1
--- /dev/null
+++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::Settings::DeployKeysPresenter do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ subject(:presenter) do
+ described_class.new(project, current_user: user)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Simple' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Simple)
+ end
+
+ describe '#enabled_keys' do
+ it 'returns currently enabled keys' do
+ expect(presenter.enabled_keys).to eq [deploy_keys_project.deploy_key]
+ end
+
+ it 'does not contain enabled_keys inside available_keys' do
+ expect(presenter.available_keys).not_to include deploy_key
+ end
+
+ it 'returns the enabled_keys size' do
+ expect(presenter.enabled_keys_size).to eq(1)
+ end
+
+ it 'returns true if there is any enabled_keys' do
+ expect(presenter.any_keys_enabled?).to eq(true)
+ end
+ end
+
+ describe '#available_keys/#available_project_keys' do
+ let(:other_deploy_key) { create(:another_deploy_key) }
+
+ before do
+ project_key = create(:deploy_keys_project, deploy_key: other_deploy_key)
+ project_key.project.add_developer(user)
+ end
+
+ it 'returns the current available_keys' do
+ expect(presenter.available_keys).not_to be_empty
+ end
+
+ it 'returns the current available_project_keys' do
+ expect(presenter.available_project_keys).not_to be_empty
+ end
+
+ it 'returns false if any available_project_keys are enabled' do
+ expect(presenter.any_available_project_keys_enabled?).to eq(true)
+ end
+
+ it 'returns the available_project_keys size' do
+ expect(presenter.available_project_keys_size).to eq(1)
+ end
+
+ it 'shows if there is an available key' do
+ expect(presenter.key_available?(deploy_key)).to eq(false)
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 919c98d6437..46edbd49b28 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -200,7 +200,7 @@ describe API::AccessRequests, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
end
@@ -210,7 +210,7 @@ describe API::AccessRequests, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
index be4bc39ada2..f5265ea60ff 100644
--- a/spec/requests/api/api_internal_helpers_spec.rb
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -21,7 +21,7 @@ describe ::API::Helpers::InternalHelpers do
# Relative and absolute storage paths, with and without trailing /
['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
context "storage path is #{storage_path}" do
- subject { clean_project_path(project_path, [storage_path]) }
+ subject { clean_project_path(project_path, [{ 'path' => storage_path }]) }
it { is_expected.to eq(expected) }
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 6cc1ef315db..f4d4a8a2cc7 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -15,7 +15,7 @@ describe API::AwardEmoji, api: true do
describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
context 'on an issue' do
it "returns an array of award_emoji" do
- get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -31,7 +31,7 @@ describe API::AwardEmoji, api: true do
context 'on a merge request' do
it "returns an array of award_emoji" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -57,7 +57,7 @@ describe API::AwardEmoji, api: true do
it 'returns a status code 404' do
user1 = create(:user)
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1)
expect(response).to have_http_status(404)
end
@@ -68,7 +68,7 @@ describe API::AwardEmoji, api: true do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an array of award emoji' do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -79,7 +79,7 @@ describe API::AwardEmoji, api: true do
describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
context 'on an issue' do
it "returns the award emoji" do
- get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(award_emoji.name)
@@ -88,7 +88,7 @@ describe API::AwardEmoji, api: true do
end
it "returns a 404 error if the award is not found" do
- get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
expect(response).to have_http_status(404)
end
@@ -96,7 +96,7 @@ describe API::AwardEmoji, api: true do
context 'on a merge request' do
it 'returns the award emoji' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(downvote.name)
@@ -123,7 +123,7 @@ describe API::AwardEmoji, api: true do
it 'returns a status code 404' do
user1 = create(:user)
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1)
expect(response).to have_http_status(404)
end
@@ -134,7 +134,7 @@ describe API::AwardEmoji, api: true do
let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
it 'returns an award emoji' do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
expect(response).to have_http_status(200)
expect(json_response).not_to be_an Array
@@ -147,7 +147,7 @@ describe API::AwardEmoji, api: true do
context "on an issue" do
it "creates a new award emoji" do
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish'
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('blowfish')
@@ -155,13 +155,13 @@ describe API::AwardEmoji, api: true do
end
it "returns a 400 bad request error if the name is not given" do
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
expect(response).to have_http_status(400)
end
it "returns a 401 unauthorized error if the user is not authenticated" do
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup'
expect(response).to have_http_status(401)
end
@@ -173,15 +173,15 @@ describe API::AwardEmoji, api: true do
end
it "normalizes +1 as thumbsup award" do
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: '+1'
expect(issue.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
- post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
@@ -207,7 +207,7 @@ describe API::AwardEmoji, api: true do
it 'creates a new award emoji' do
expect do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
end.to change { note.award_emoji.count }.from(0).to(1)
expect(response).to have_http_status(201)
@@ -215,21 +215,21 @@ describe API::AwardEmoji, api: true do
end
it "it returns 404 error when user authored note" do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
expect(response).to have_http_status(404)
end
it "normalizes +1 as thumbsup award" do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: '+1'
expect(note.award_emoji.last.name).to eq("thumbsup")
end
context 'when the emoji already has been awarded' do
it 'returns a 404 status code' do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
- post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
expect(response).to have_http_status(404)
expect(json_response["message"]).to match("has already been taken")
@@ -241,14 +241,14 @@ describe API::AwardEmoji, api: true do
context 'when the awardable is an Issue' do
it 'deletes the award' do
expect do
- delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
- end.to change { issue.award_emoji.count }.from(1).to(0)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when the award emoji can not be found' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
expect(response).to have_http_status(404)
end
@@ -257,14 +257,14 @@ describe API::AwardEmoji, api: true do
context 'when the awardable is a Merge Request' do
it 'deletes the award' do
expect do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
- end.to change { merge_request.award_emoji.count }.from(1).to(0)
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when note id not found' do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user)
expect(response).to have_http_status(404)
end
@@ -277,9 +277,9 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- end.to change { snippet.award_emoji.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
end
end
end
@@ -289,10 +289,10 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
- delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- end.to change { note.award_emoji.count }.from(1).to(0)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { note.award_emoji.count }.from(1).to(0)
end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 71df534ebe1..87c36639cd4 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -195,8 +195,7 @@ describe API::Boards, api: true do
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
- expect(response).to have_http_status(200)
- expect(json_response['position']).to eq(1)
+ expect(response).to have_http_status(204)
end
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5571f6cc107..a70f7beaae0 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,77 +5,146 @@ describe API::Branches, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+ let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] }
describe "GET /projects/:id/repository/branches" do
- it "returns an array of project branches" do
- project.repository.expire_all_method_caches
+ let(:route) { "/projects/#{project.id}/repository/branches" }
- get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+ shared_examples_for 'repository branches' do
+ it 'returns the repository branches' do
+ get api(route, current_user), per_page: 100
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- branch_names = json_response.map { |x| x['name'] }
- expect(branch_names).to match_array(project.repository.branch_names)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ branch_names = json_response.map { |x| x['name'] }
+ expect(branch_names).to match_array(project.repository.branch_names)
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
- end
- describe "GET /projects/:id/repository/branches/:branch" do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branches' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
- expect(json_response['name']).to eq(branch_name)
- json_commit = json_response['commit']
- expect(json_commit['id']).to eq(branch_sha)
- expect(json_commit).to have_key('short_id')
- expect(json_commit).to have_key('title')
- expect(json_commit).to have_key('message')
- expect(json_commit).to have_key('author_name')
- expect(json_commit).to have_key('author_email')
- expect(json_commit).to have_key('authored_date')
- expect(json_commit).to have_key('committer_name')
- expect(json_commit).to have_key('committer_email')
- expect(json_commit).to have_key('committed_date')
- expect(json_commit).to have_key('parent_ids')
- expect(json_response['merged']).to eq(false)
- expect(json_response['protected']).to eq(false)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "returns the branch information for a single branch with dots in the name" do
- get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository branches' do
+ let(:current_user) { user }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
+ end
+
+ describe "GET /projects/:id/repository/branches/:branch" do
+ let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" }
- context 'on a merged branch' do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/merge-test", user)
+ shared_examples_for 'repository branch' do |merged: false|
+ it 'returns the repository branch' do
+ get api(route, current_user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq('merge-test')
- expect(json_response['merged']).to eq(true)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['merged']).to eq(merged)
+ expect(json_response['protected']).to eq(false)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+
+ json_commit = json_response['commit']
+ expect(json_commit['id']).to eq(branch_sha)
+ expect(json_commit).to have_key('short_id')
+ expect(json_commit).to have_key('title')
+ expect(json_commit).to have_key('message')
+ expect(json_commit).to have_key('author_name')
+ expect(json_commit).to have_key('author_email')
+ expect(json_commit).to have_key('authored_date')
+ expect(json_commit).to have_key('committer_name')
+ expect(json_commit).to have_key('committer_email')
+ expect(json_commit).to have_key('committed_date')
+ expect(json_commit).to have_key('parent_ids')
+ end
+
+ context 'when branch does not exist' do
+ let(:branch_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- it "returns a 403 error if guest" do
- get api("/projects/#{project.id}/repository/branches", user2)
- expect(response).to have_http_status(403)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branch' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- it "returns a 404 error if branch is not available" do
- get api("/projects/#{project.id}/repository/branches/unknown", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+ it_behaves_like 'repository branch'
+
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot.name }
+ let(:branch_sha) { project.commit('master').sha }
+
+ it_behaves_like 'repository branch'
+ end
+
+ context 'when branch is merged' do
+ let(:branch_name) { 'merge-test' }
+ let(:branch_sha) { project.commit('merge-test').sha }
+
+ it_behaves_like 'repository branch', merged: true
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
@@ -93,10 +162,10 @@ describe API::Branches, api: true do
end
it "protects a single branch with dots in the name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(true)
end
@@ -234,7 +303,7 @@ describe API::Branches, api: true do
end
it "returns a 403 error if guest" do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest)
expect(response).to have_http_status(403)
end
end
@@ -250,10 +319,10 @@ describe API::Branches, api: true do
end
it "update branches with dots in branch name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(false)
end
@@ -282,7 +351,7 @@ describe API::Branches, api: true do
end
it "denies for user without push access" do
- post api("/projects/#{project.id}/repository/branches", user2),
+ post api("/projects/#{project.id}/repository/branches", guest),
branch: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
@@ -325,15 +394,14 @@ describe API::Branches, api: true do
it "removes branch" do
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
- expect(json_response['branch']).to eq(branch_name)
+
+ expect(response).to have_http_status(204)
end
it "removes a branch with dots in the branch name" do
- delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
- expect(response).to have_http_status(200)
- expect(json_response['branch']).to eq("with.1.2.3")
+ expect(response).to have_http_status(204)
end
it 'returns 404 if branch not exists' do
@@ -360,13 +428,15 @@ describe API::Branches, api: true do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
- it 'returns 200' do
+ it 'returns 202 with json body' do
delete api("/projects/#{project.id}/repository/merged_branches", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'returns a 403 error if guest' do
- delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ delete api("/projects/#{project.id}/repository/merged_branches", guest)
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 921d8714173..024fa66848c 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -174,8 +174,11 @@ describe API::BroadcastMessages, api: true do
end
it 'deletes the broadcast message for admins' do
- expect { delete api("/broadcast_messages/#{message.id}", admin) }
- .to change { BroadcastMessage.count }.by(-1)
+ expect do
+ delete api("/broadcast_messages/#{message.id}", admin)
+
+ expect(response).to have_http_status(204)
+ end.to change { BroadcastMessage.count }.by(-1)
end
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
deleted file mode 100644
index 38aef7f2767..00000000000
--- a/spec/requests/api/builds_spec.rb
+++ /dev/null
@@ -1,475 +0,0 @@
-require 'spec_helper'
-
-describe API::Builds, api: true do
- include ApiHelpers
-
- let(:user) { create(:user) }
- let(:api_user) { user }
- let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
- let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- let(:reporter) { create(:project_member, :reporter, project: project) }
- let(:guest) { create(:project_member, :guest, project: project) }
- let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
- let!(:build) { create(:ci_build, pipeline: pipeline) }
-
- describe 'GET /projects/:id/builds ' do
- let(:query) { '' }
-
- before do
- get api("/projects/#{project.id}/builds?#{query}", api_user)
- end
-
- context 'authorized user' do
- it 'returns project builds' do
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- end
-
- it 'returns pipeline data' do
- json_build = json_response.first
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
-
- context 'filter project with one scope element' do
- let(:query) { 'scope=pending' }
-
- it do
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'filter project with array of scope elements' do
- let(:query) { 'scope[0]=pending&scope[1]=running' }
-
- it do
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- let(:query) { 'scope[0]=pending&scope[1]=unknown_status' }
-
- it { expect(response).to have_http_status(400) }
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return project builds' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- describe 'GET /projects/:id/repository/commits/:sha/builds' do
- context 'when commit does not exist in repository' do
- before do
- get api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
- end
-
- it 'responds with 404' do
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when commit exists in repository' do
- context 'when user is authorized' do
- context 'when pipeline has jobs' do
- before do
- create(:ci_pipeline, project: project, sha: project.commit.id)
- create(:ci_build, pipeline: pipeline)
- create(:ci_build)
-
- get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
- end
-
- it 'returns project jobs for specific commit' do
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq 2
- end
-
- it 'returns pipeline data' do
- json_build = json_response.first
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
- end
-
- context 'when pipeline has no jobs' do
- before do
- branch_head = project.commit('feature').id
- get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
- end
-
- it 'returns an empty array' do
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response).to be_empty
- end
- end
- end
-
- context 'when user is not authorized' do
- before do
- create(:ci_pipeline, project: project, sha: project.commit.id)
- create(:ci_build, pipeline: pipeline)
-
- get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
- end
-
- it 'does not return project jobs' do
- expect(response).to have_http_status(401)
- expect(json_response.except('message')).to be_empty
- end
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id' do
- before do
- get api("/projects/#{project.id}/builds/#{build.id}", api_user)
- end
-
- context 'authorized user' do
- it 'returns specific job data' do
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq('test')
- end
-
- it 'returns pipeline data' do
- json_build = json_response
- expect(json_build['pipeline']).not_to be_empty
- expect(json_build['pipeline']['id']).to eq build.pipeline.id
- expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
- expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
- expect(json_build['pipeline']['status']).to eq build.pipeline.status
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job data' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id/artifacts' do
- before do
- get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
- end
-
- context 'job with artifacts' do
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- context 'authorized user' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
-
- it 'returns specific job artifacts' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include(download_headers)
- expect(response.body).to match_file(build.artifacts_file.file.file)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job artifacts' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- it 'does not return job artifacts if not uploaded' do
- expect(response).to have_http_status(404)
- end
- end
-
- describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
- let(:api_user) { reporter.user }
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- before do
- build.success
- end
-
- def path_for_ref(ref = pipeline.ref, job = build.name)
- api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
- end
-
- context 'when not logged in' do
- let(:api_user) { nil }
-
- before do
- get path_for_ref
- end
-
- it 'gives 401' do
- expect(response).to have_http_status(401)
- end
- end
-
- context 'when logging as guest' do
- let(:api_user) { guest.user }
-
- before do
- get path_for_ref
- end
-
- it 'gives 403' do
- expect(response).to have_http_status(403)
- end
- end
-
- context 'non-existing job' do
- shared_examples 'not found' do
- it { expect(response).to have_http_status(:not_found) }
- end
-
- context 'has no such ref' do
- before do
- get path_for_ref('TAIL', build.name)
- end
-
- it_behaves_like 'not found'
- end
-
- context 'has no such job' do
- before do
- get path_for_ref(pipeline.ref, 'NOBUILD')
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'find proper job' do
- shared_examples 'a valid file' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' =>
- "attachment; filename=#{build.artifacts_file.filename}" }
- end
-
- it { expect(response).to have_http_status(200) }
- it { expect(response.headers).to include(download_headers) }
- end
-
- context 'with regular branch' do
- before do
- pipeline.reload
- pipeline.update(ref: 'master',
- sha: project.commit('master').sha)
-
- get path_for_ref('master')
- end
-
- it_behaves_like 'a valid file'
- end
-
- context 'with branch name containing slash' do
- before do
- pipeline.reload
- pipeline.update(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
- end
-
- before do
- get path_for_ref('improve/awesome')
- end
-
- it_behaves_like 'a valid file'
- end
- end
- end
-
- describe 'GET /projects/:id/builds/:build_id/trace' do
- let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
-
- before do
- get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
- end
-
- context 'authorized user' do
- it 'returns specific job trace' do
- expect(response).to have_http_status(200)
- expect(response.body).to eq(build.trace)
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not return specific job trace' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/cancel' do
- before do
- post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
- end
-
- context 'authorized user' do
- context 'user with :update_build persmission' do
- it 'cancels running or pending job' do
- expect(response).to have_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
- end
- end
-
- context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
-
- it 'does not cancel job' do
- expect(response).to have_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not cancel job' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/retry' do
- let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
-
- before do
- post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
- end
-
- context 'authorized user' do
- context 'user with :update_build permission' do
- it 'retries non-running job' do
- expect(response).to have_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
- expect(json_response['status']).to eq('pending')
- end
- end
-
- context 'user without :update_build permission' do
- let(:api_user) { reporter.user }
-
- it 'does not retry job' do
- expect(response).to have_http_status(403)
- end
- end
- end
-
- context 'unauthorized user' do
- let(:api_user) { nil }
-
- it 'does not retry job' do
- expect(response).to have_http_status(401)
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/erase' do
- before do
- post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
- end
-
- context 'job is erasable' do
- let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
-
- it 'erases job content' do
- expect(response.status).to eq 201
- expect(build.trace).to be_empty
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
- end
-
- it 'updates job' do
- expect(build.reload.erased_at).to be_truthy
- expect(build.reload.erased_by).to eq user
- end
- end
-
- context 'job is not erasable' do
- let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
-
- it 'responds with forbidden' do
- expect(response.status).to eq 403
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
- before do
- post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
- end
-
- context 'artifacts did not expire' do
- let(:build) do
- create(:ci_build, :trace, :artifacts, :success,
- project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
- end
-
- it 'keeps artifacts' do
- expect(response.status).to eq 200
- expect(build.reload.artifacts_expire_at).to be_nil
- end
- end
-
- context 'no artifacts' do
- let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
-
- it 'responds with not found' do
- expect(response.status).to eq 404
- end
- end
- end
-
- describe 'POST /projects/:id/builds/:build_id/play' do
- before do
- post api("/projects/#{project.id}/builds/#{build.id}/play", user)
- end
-
- context 'on an playable job' do
- let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
-
- it 'plays the job' do
- expect(response).to have_http_status 200
- expect(json_response['user']['id']).to eq(user.id)
- expect(json_response['id']).to eq(build.id)
- end
- end
-
- context 'on a non-playable job' do
- it 'returns a status code 400, Bad Request' do
- expect(response).to have_http_status 400
- expect(response.body).to match("Unplayable Job")
- end
- end
- end
-end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 81a8856b8f1..d8b3cc041a5 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -151,26 +151,62 @@ describe API::CommitStatuses, api: true do
end
context 'with all optional parameters' do
- before do
- optional_params = { state: 'success',
- context: 'coverage',
- ref: 'develop',
- description: 'test',
- coverage: 80.0,
- target_url: 'http://gitlab.com/status' }
-
- post api(post_url, developer), optional_params
+ context 'when creating a commit status' do
+ it 'creates commit status' do
+ post api(post_url, developer), {
+ state: 'success',
+ context: 'coverage',
+ ref: 'develop',
+ description: 'test',
+ coverage: 80.0,
+ target_url: 'http://gitlab.com/status'
+ }
+
+ expect(response).to have_http_status(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq('success')
+ expect(json_response['name']).to eq('coverage')
+ expect(json_response['ref']).to eq('develop')
+ expect(json_response['coverage']).to eq(80.0)
+ expect(json_response['description']).to eq('test')
+ expect(json_response['target_url']).to eq('http://gitlab.com/status')
+ end
end
- it 'creates commit status' do
- expect(response).to have_http_status(201)
- expect(json_response['sha']).to eq(commit.id)
- expect(json_response['status']).to eq('success')
- expect(json_response['name']).to eq('coverage')
- expect(json_response['ref']).to eq('develop')
- expect(json_response['coverage']).to eq(80.0)
- expect(json_response['description']).to eq('test')
- expect(json_response['target_url']).to eq('http://gitlab.com/status')
+ context 'when updatig a commit status' do
+ before do
+ post api(post_url, developer), {
+ state: 'running',
+ context: 'coverage',
+ ref: 'develop',
+ description: 'coverage test',
+ coverage: 0.0,
+ target_url: 'http://gitlab.com/status'
+ }
+
+ post api(post_url, developer), {
+ state: 'success',
+ name: 'coverage',
+ ref: 'develop',
+ description: 'new description',
+ coverage: 90.0
+ }
+ end
+
+ it 'updates a commit status' do
+ expect(response).to have_http_status(201)
+ expect(json_response['sha']).to eq(commit.id)
+ expect(json_response['status']).to eq('success')
+ expect(json_response['name']).to eq('coverage')
+ expect(json_response['ref']).to eq('develop')
+ expect(json_response['coverage']).to eq(90.0)
+ expect(json_response['description']).to eq('new description')
+ expect(json_response['target_url']).to eq('http://gitlab.com/status')
+ end
+
+ it 'does not create a new commit status' do
+ expect(CommitStatus.count).to eq 1
+ end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 8b3dfedc5a9..a10d876ffad 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -19,6 +19,7 @@ describe API::Commits, api: true do
it "returns project commits" do
commit = project.repository.commit
+
get api("/projects/#{project.id}/repository/commits", user)
expect(response).to have_http_status(200)
@@ -27,6 +28,16 @@ describe API::Commits, api: true do
expect(json_response.first['committer_name']).to eq(commit.committer_name)
expect(json_response.first['committer_email']).to eq(commit.committer_email)
end
+
+ it 'include correct pagination headers' do
+ commit_count = project.repository.count_commits(ref: 'master').to_s
+
+ get api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "unauthorized user" do
@@ -39,14 +50,26 @@ describe API::Commits, api: true do
context "since optional parameter" do
it "returns project commits since provided parameter" do
commits = project.repository.commits("master")
- since = commits.second.created_at
+ after = commits.second.created_at
- get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
+ get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
expect(json_response.size).to eq 2
expect(json_response.first["id"]).to eq(commits.first.id)
expect(json_response.second["id"]).to eq(commits.second.id)
end
+
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ after = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
+
+ get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "until optional parameter" do
@@ -65,6 +88,18 @@ describe API::Commits, api: true do
expect(json_response.first["id"]).to eq(commits.second.id)
expect(json_response.second["id"]).to eq(commits.third.id)
end
+
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
+
+ get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "invalid xmlschema date parameters" do
@@ -79,16 +114,71 @@ describe API::Commits, api: true do
context "path optional parameter" do
it "returns project commits matching provided path parameter" do
path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
expect(json_response.size).to eq(3)
expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ end
+
+ it 'include correct pagination headers' do
+ path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
+
+ get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
+
+ context 'with pagination params' do
+ let(:page) { 1 }
+ let(:per_page) { 5 }
+ let(:ref_name) { 'master' }
+ let!(:request) do
+ get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ end
+
+ it 'returns correct headers' do
+ commit_count = project.repository.count_commits(ref: ref_name).to_s
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eq('1')
+ expect(response.headers['Link']).to match(/page=1&per_page=5/)
+ expect(response.headers['Link']).to match(/page=2&per_page=5/)
+ end
+
+ context 'viewing the first page' do
+ it 'returns the first 5 commits' do
+ commit = project.repository.commit
+
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('1')
+ end
+ end
+
+ context 'viewing the third page' do
+ let(:page) { 3 }
+
+ it 'returns the third 5 commits' do
+ commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
+
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('3')
+ end
end
end
end
- describe "Create a commit with multiple files and actions" do
+ describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project.id}/repository/commits" }
it 'returns a 403 unauthorized for user without permissions' do
@@ -103,7 +193,7 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
- context :create do
+ describe 'create' do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
@@ -147,8 +237,8 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
- context 'with project path in URL' do
- let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+ context 'with project path containing a dot in URL' do
+ let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
@@ -158,7 +248,7 @@ describe API::Commits, api: true do
end
end
- context :delete do
+ describe 'delete' do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
@@ -199,7 +289,7 @@ describe API::Commits, api: true do
end
end
- context :move do
+ describe 'move' do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
@@ -244,7 +334,7 @@ describe API::Commits, api: true do
end
end
- context :update do
+ describe 'update' do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
@@ -287,7 +377,7 @@ describe API::Commits, api: true do
end
end
- context "multiple operations" do
+ describe 'multiple operations' do
let(:message) { 'Multiple actions' }
let!(:invalid_mo_params) do
{
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 7e682e91bd1..4f4b18cf0e0 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -116,6 +116,8 @@ describe API::DeployKeys, api: true do
it 'should delete existing key' do
expect do
delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ project.deploy_keys.count }.by(-1)
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index bd9ecaf2685..f6fd567eca5 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -1,17 +1,23 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::API, api: true do
include ApiHelpers
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
- describe "when unauthenticated" do
+ describe "unauthenticated" do
it "returns authentication success" do
get api("/user"), access_token: token.token
expect(response).to have_http_status(200)
end
+
+ include_examples 'user login request with unique ip limit' do
+ def request
+ get api('/user'), access_token: token.token
+ end
+ end
end
describe "when token invalid" do
@@ -26,5 +32,29 @@ describe API::API, api: true do
get api("/user", user)
expect(response).to have_http_status(200)
end
+
+ include_examples 'user login request with unique ip limit' do
+ def request
+ get api('/user', user)
+ end
+ end
+ end
+
+ describe "when user is blocked" do
+ it "returns authentication error" do
+ user.block
+ get api("/user"), access_token: token.token
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ describe "when user is ldap_blocked" do
+ it "returns authentication error" do
+ user.ldap_block
+ get api("/user"), access_token: token.token
+
+ expect(response).to have_http_status(401)
+ end
end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index d0958d39d44..b54ee8e8b85 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -15,6 +15,8 @@ describe API::Environments, api: true do
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
it 'returns project environments' do
+ project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+
get api("/projects/#{project.id}/environments", user)
expect(response).to have_http_status(200)
@@ -23,7 +25,7 @@ describe API::Environments, api: true do
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
- expect(json_response.first['project']['id']).to eq(project.id)
+ expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
end
end
@@ -122,7 +124,7 @@ describe API::Environments, api: true do
it 'returns a 200 for an existing environment' do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it 'returns a 404 for non existing id' do
@@ -141,4 +143,39 @@ describe API::Environments, api: true do
end
end
end
+
+ describe 'POST /projects/:id/environments/:environment_id/stop' do
+ context 'as a master' do
+ context 'with a stoppable environment' do
+ before do
+ environment.update(state: :available)
+
+ post api("/projects/#{project.id}/environments/#{environment.id}/stop", user)
+ end
+
+ it 'returns a 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'actually stops the environment' do
+ expect(environment.reload).to be_stopped
+ end
+ end
+
+ it 'returns a 404 for non existing id' do
+ post api("/projects/#{project.id}/environments/12345/stop", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ post api("/projects/#{project.id}/environments/#{environment.id}/stop", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index a8ce0430401..a7fad7f0bdb 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,10 +5,9 @@ describe API::Files, api: true do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { 'files/ruby/popen.rb' }
+ let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
let(:params) do
{
- file_path: file_path,
ref: 'master'
}
end
@@ -30,36 +29,54 @@ describe API::Files, api: true do
before { project.team << [user, :developer] }
- describe "GET /projects/:id/repository/files" do
- let(:route) { "/projects/#{project.id}/repository/files" }
+ def route(file_path = nil)
+ "/projects/#{project.id}/repository/files/#{file_path}"
+ end
+ describe "GET /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
- it "returns file info" do
- get api(route, current_user), params
+ it 'returns file attributes as json' do
+ get api(route(file_path), current_user), params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
- context 'when no params are given' do
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+
+ get api(route(file_path), current_user), params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_name']).to eq('commit.js.coffee')
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+ end
+
+ it 'returns raw file info' do
+ url = route(file_path) + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(url, current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route, current_user) }
+ let(:request) { get api(route("any%2Ffile"), current_user) }
end
end
context 'when file_path does not exist' do
- let(:params) do
- {
- file_path: 'app/models/application.rb',
- ref: 'master',
- }
- end
+ let(:params) { { ref: 'master' } }
it_behaves_like '404 response' do
- let(:request) { get api(route, current_user), params }
+ let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
let(:message) { '404 File Not Found' }
end
end
@@ -68,7 +85,7 @@ describe API::Files, api: true do
include_context 'disabled repository'
it_behaves_like '403 response' do
- let(:request) { get api(route, current_user), params }
+ let(:request) { get api(route(file_path), current_user), params }
end
end
end
@@ -82,7 +99,7 @@ describe API::Files, api: true do
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
- let(:request) { get api(route), params }
+ let(:request) { get api(route(file_path)), params }
let(:message) { '404 Project Not Found' }
end
end
@@ -95,42 +112,115 @@ describe API::Files, api: true do
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
- let(:request) { get api(route, guest), params }
+ let(:request) { get api(route(file_path), guest), params }
+ end
+ end
+ end
+
+ describe "GET /projects/:id/repository/files/:file_path/raw" do
+ shared_examples_for 'repository raw files' do
+ it 'returns raw file info' do
+ url = route(file_path) + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(url, current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(route(file_path) + "/raw", current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when mandatory params are not given' do
+ it_behaves_like '400 response' do
+ let(:request) { get api(route("any%2Ffile"), current_user) }
+ end
+ end
+
+ context 'when file_path does not exist' do
+ let(:params) { { ref: 'master' } }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), current_user), params }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository raw files' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository raw files' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params }
end
end
end
- describe "POST /projects/:id/repository/files" do
+ describe "POST /projects/:id/repository/files/:file_path" do
+ let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
let(:valid_params) do
{
- file_path: 'newfile.rb',
- branch: 'master',
- content: 'puts 8',
- commit_message: 'Added newfile'
+ branch: "master",
+ content: "puts 8",
+ commit_message: "Added newfile"
}
end
it "creates a new file in project repo" do
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route(file_path), user), valid_params
expect(response).to have_http_status(201)
- expect(json_response['file_path']).to eq('newfile.rb')
+ expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
- it "returns a 400 bad request if no params given" do
- post api("/projects/#{project.id}/repository/files", user)
+ it "returns a 400 bad request if no mandatory params given" do
+ post api(route("any%2Etxt"), user)
expect(response).to have_http_status(400)
end
it "returns a 400 if editor fails to create file" do
- allow_any_instance_of(Repository).to receive(:commit_file).
+ allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route("any%2Etxt"), user), valid_params
expect(response).to have_http_status(400)
end
@@ -139,7 +229,7 @@ describe API::Files, api: true do
it "creates a new file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route("new_file_with_author%2Etxt"), user), valid_params
expect(response).to have_http_status(201)
last_commit = project.repository.commit.raw
@@ -147,12 +237,25 @@ describe API::Files, api: true do
expect(last_commit.author_name).to eq(author_name)
end
end
+
+ context 'when the repo is empty' do
+ let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+
+ it "creates a new file in project repo" do
+ post api(route("newfile%2Erb"), user), valid_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['file_path']).to eq('newfile.rb')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ end
+ end
end
describe "PUT /projects/:id/repository/files" do
let(:valid_params) do
{
- file_path: file_path,
branch: 'master',
content: 'puts 8',
commit_message: 'Changed file'
@@ -160,17 +263,17 @@ describe API::Files, api: true do
end
it "updates existing file in project repo" do
- put api("/projects/#{project.id}/repository/files", user), valid_params
+ put api(route(file_path), user), valid_params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
- put api("/projects/#{project.id}/repository/files", user)
+ put api(route(file_path), user)
expect(response).to have_http_status(400)
end
@@ -179,7 +282,7 @@ describe API::Files, api: true do
it "updates a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
- put api("/projects/#{project.id}/repository/files", user), valid_params
+ put api(route(file_path), user), valid_params
expect(response).to have_http_status(200)
last_commit = project.repository.commit.raw
@@ -192,32 +295,27 @@ describe API::Files, api: true do
describe "DELETE /projects/:id/repository/files" do
let(:valid_params) do
{
- file_path: file_path,
branch: 'master',
commit_message: 'Changed file'
}
end
it "deletes existing file in project repo" do
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
- expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
+ expect(response).to have_http_status(204)
end
it "returns a 400 bad request if no params given" do
- delete api("/projects/#{project.id}/repository/files", user)
+ delete api(route(file_path), user)
expect(response).to have_http_status(400)
end
it "returns a 400 if fails to create file" do
- allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
+ allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
expect(response).to have_http_status(400)
end
@@ -226,21 +324,17 @@ describe API::Files, api: true do
it "removes a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
- expect(response).to have_http_status(200)
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(author_email)
- expect(last_commit.author_name).to eq(author_name)
+ expect(response).to have_http_status(204)
end
end
end
describe "POST /projects/:id/repository/files with binary file" do
- let(:file_path) { 'test.bin' }
+ let(:file_path) { 'test%2Ebin' }
let(:put_params) do
{
- file_path: file_path,
branch: 'master',
content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
commit_message: 'Binary file with a \n should not be touched',
@@ -249,21 +343,20 @@ describe API::Files, api: true do
end
let(:get_params) do
{
- file_path: file_path,
ref: 'master',
}
end
before do
- post api("/projects/#{project.id}/repository/files", user), put_params
+ post api(route(file_path), user), put_params
end
it "remains unchanged" do
- get api("/projects/#{project.id}/repository/files", user), get_params
+ get api(route(file_path), user), get_params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
- expect(json_response['file_name']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ expect(json_response['file_name']).to eq(CGI.unescape(file_path))
expect(json_response['content']).to eq(put_params[:content])
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index a59112579e5..2545da7b1db 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -76,6 +76,8 @@ describe API::Groups, api: true do
lfs_objects_size: 234,
build_artifacts_size: 345,
}.stringify_keys
+ exposed_attributes = attributes.dup
+ exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
project1.statistics.update!(attributes)
@@ -85,7 +87,7 @@ describe API::Groups, api: true do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response)
- .to satisfy_one { |group| group['statistics'] == attributes }
+ .to satisfy_one { |group| group['statistics'] == exposed_attributes }
end
end
@@ -150,20 +152,10 @@ describe API::Groups, api: true do
expect(response_groups).to eq([group1.name, group3.name])
end
end
- end
-
- describe 'GET /groups/owned' do
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/groups/owned')
-
- expect(response).to have_http_status(401)
- end
- end
- context 'when authenticated as group owner' do
+ context 'when using owned in the request' do
it 'returns an array of groups the user owns' do
- get api('/groups/owned', user2)
+ get api('/groups', user2), owned: true
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -186,7 +178,7 @@ describe API::Groups, api: true do
expect(json_response['name']).to eq(group1.name)
expect(json_response['path']).to eq(group1.path)
expect(json_response['description']).to eq(group1.description)
- expect(json_response['visibility_level']).to eq(group1.visibility_level)
+ expect(json_response['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
expect(json_response['avatar_url']).to eq(group1.avatar_url)
expect(json_response['web_url']).to eq(group1.web_url)
expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
@@ -303,9 +295,9 @@ describe API::Groups, api: true do
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
- project_names = json_response.map { |proj| proj['name' ] }
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['visibility_level']).to be_present
+ expect(json_response.first['visibility']).to be_present
end
it "returns the group's projects with simple representation" do
@@ -314,9 +306,9 @@ describe API::Groups, api: true do
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
- project_names = json_response.map { |proj| proj['name' ] }
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['visibility_level']).not_to be_present
+ expect(json_response.first['visibility']).not_to be_present
end
it 'filters the groups projects' do
@@ -398,7 +390,7 @@ describe API::Groups, api: true do
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
- project_names = json_response.map { |proj| proj['name' ] }
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
end
@@ -477,7 +469,7 @@ describe API::Groups, api: true do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it "does not remove a group if not an owner" do
@@ -506,7 +498,7 @@ describe API::Groups, api: true do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it "does not remove a non existing group" do
@@ -519,7 +511,7 @@ describe API::Groups, api: true do
describe "POST /groups/:id/projects/:project_id" do
let(:project) { create(:empty_project) }
- let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+ let(:project_path) { project.full_path.gsub('/', '%2F') }
before(:each) do
allow_any_instance_of(Projects::TransferService).
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a89676fec93..988a57a80ea 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -436,7 +436,7 @@ describe API::Helpers, api: true do
context 'current_user is present' do
before do
- expect_any_instance_of(self.class).to receive(:current_user).and_return(true)
+ expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new)
end
it 'does not raise an error' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index ffeacb15f17..63ec00cdf04 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -397,16 +397,53 @@ describe API::Internal, api: true do
before do
project.team << [user, :developer]
- get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
end
it 'returns link to create new merge request' do
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
expect(json_response).to match [{
"branch_name" => "new_branch",
"url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
+
+ it 'returns empty array if printing_merge_request_link_enabled is false' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
+ expect(json_response).to eq([])
+ end
+ end
+
+ describe 'POST /notify_post_receive' do
+ let(:valid_params) do
+ { repo_path: project.repository.path, secret_token: secret_token }
+ end
+
+ before do
+ allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
+ end
+
+ it "calls the Gitaly client if it's enabled" do
+ expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+ to receive(:post_receive).with(project.repository.path)
+
+ post api("/internal/notify_post_receive"), valid_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 500 if the gitaly call fails" do
+ expect_any_instance_of(Gitlab::GitalyClient::Notifications).
+ to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable)
+
+ post api("/internal/notify_post_receive"), valid_params
+
+ expect(response).to have_http_status(500)
+ end
end
def project_with_repo_path(path)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 56ca4c04e7d..52f68fed2cc 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -153,6 +153,16 @@ describe API::Issues, api: true do
expect(json_response.first['state']).to eq('opened')
end
+ it 'returns unlabeled issues for "No Label" label' do
+ get api("/issues", user), labels: 'No Label'
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to be_empty
+ end
+
it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
@@ -212,6 +222,25 @@ describe API::Issues, api: true do
expect(json_response.first['id']).to eq(confidential_issue.id)
end
+ it 'returns an array of issues found by iids' do
+ get api('/issues', user), iids: [closed_issue.iid]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api("/issues", user), iids: [99999]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
it 'sorts by created_at descending by default' do
get api('/issues', user)
@@ -251,6 +280,13 @@ describe API::Issues, api: true do
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
+
+ it 'matches V4 response schema' do
+ get api('/issues', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ end
end
end
@@ -377,6 +413,25 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
+ it 'returns an array of issues found by iids' do
+ get api(base_url, user), iids: [group_issue.iid]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api(base_url, user), iids: [99999]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
it 'returns an empty array if no group issue matches labels' do
get api("#{base_url}?labels=foo,bar", user)
@@ -479,6 +534,12 @@ describe API::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ it 'returns 404 when project does not exist' do
+ get api('/projects/1000/issues', non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
@@ -586,6 +647,25 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
+ it 'returns an array of issues found by iids' do
+ get api("#{base_url}/issues", user), iids: [issue.iid]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api("#{base_url}/issues", user), iids: [99999]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
it 'returns an empty array if not all labels matches' do
get api("#{base_url}/issues?labels=#{label.title},foo", user)
@@ -693,9 +773,9 @@ describe API::Issues, api: true do
end
end
- describe "GET /projects/:id/issues/:issue_id" do
+ describe "GET /projects/:id/issues/:issue_iid" do
it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.id}", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(issue.id)
@@ -713,8 +793,8 @@ describe API::Issues, api: true do
expect(json_response['confidential']).to be_falsy
end
- it "returns a project issue by id" do
- get api("/projects/#{project.id}/issues/#{issue.id}", user)
+ it "returns a project issue by internal id" do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(issue.title)
@@ -726,40 +806,52 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
+ it "returns 404 if the issue ID is used" do
+ get api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(404)
+ end
+
context 'confidential issues' do
it "returns 404 for non project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
+
expect(response).to have_http_status(404)
end
it "returns 404 for project members with guest role" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
+
expect(response).to have_http_status(404)
end
it "returns confidential issue for project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for author" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for assignee" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
it "returns confidential issue for admin" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
@@ -775,7 +867,7 @@ describe API::Issues, api: true do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
end
@@ -852,29 +944,34 @@ describe API::Issues, api: true do
])
end
- context 'resolving issues in a merge request' do
+ context 'resolving discussions' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
+
before do
project.team << [user, :master]
- post api("/projects/#{project.id}/issues", user),
- title: 'New Issue',
- merge_request_for_resolving_discussions: merge_request.iid
end
- it 'creates a new project issue' do
- expect(response).to have_http_status(:created)
- end
+ context 'resolving all discussions in a merge request' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ end
- it 'resolves the discussions in a merge request' do
- discussion.first_note.reload
-
- expect(discussion.resolved?).to be(true)
+ it_behaves_like 'creating an issue resolving discussions through the API'
end
- it 'assigns a description to the issue mentioning the merge request' do
- expect(json_response['description']).to include(merge_request.to_reference)
+ context 'resolving a single discussion' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
end
end
@@ -940,23 +1037,29 @@ describe API::Issues, api: true do
end
end
- describe "PUT /projects/:id/issues/:issue_id to update only title" do
+ describe "PUT /projects/:id/issues/:issue_iid to update only title" do
it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it "returns 404 error if issue id not found" do
+ it "returns 404 error if issue iid not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
- it 'allows special label names' do
+ it "returns 404 error if issue id is used instead of the iid" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+ expect(response).to have_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
@@ -970,40 +1073,40 @@ describe API::Issues, api: true do
context 'confidential issues' do
it "returns 403 for non project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "returns 403 for project members with guest role" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
title: 'updated title'
expect(response).to have_http_status(403)
end
it "updates a confidential issue for project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for author" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for admin" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it 'sets an issue to confidential' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
confidential: true
expect(response).to have_http_status(200)
@@ -1011,7 +1114,7 @@ describe API::Issues, api: true do
end
it 'makes a confidential issue public' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: false
expect(response).to have_http_status(200)
@@ -1019,7 +1122,7 @@ describe API::Issues, api: true do
end
it 'does not update a confidential issue with wrong confidential flag' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: 'foo'
expect(response).to have_http_status(400)
@@ -1028,7 +1131,7 @@ describe API::Issues, api: true do
end
end
- describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
+ describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
let(:params) do
{
title: 'updated title',
@@ -1041,7 +1144,7 @@ describe API::Issues, api: true do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
- put api("/projects/#{project.id}/issues/#{issue.id}", user), params
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params
expect(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -1055,12 +1158,12 @@ describe API::Issues, api: true do
end
end
- describe 'PUT /projects/:id/issues/:issue_id to update labels' do
+ describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'does not update labels if not present' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
@@ -1071,7 +1174,7 @@ describe API::Issues, api: true do
label.toggle_subscription(user2, project)
perform_enqueued_jobs do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title', labels: label.title
end
@@ -1079,14 +1182,14 @@ describe API::Issues, api: true do
end
it 'removes all labels' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
it 'updates labels' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
expect(response).to have_http_status(200)
expect(json_response['labels']).to include 'foo'
@@ -1094,7 +1197,7 @@ describe API::Issues, api: true do
end
it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo'
@@ -1108,7 +1211,7 @@ describe API::Issues, api: true do
end
it 'returns 400 if title is too long' do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'g' * 256
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
@@ -1117,9 +1220,9 @@ describe API::Issues, api: true do
end
end
- describe "PUT /projects/:id/issues/:issue_id to update state and label" do
+ describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label2', state_event: "close"
expect(response).to have_http_status(200)
@@ -1128,7 +1231,7 @@ describe API::Issues, api: true do
end
it 'reopens a project isssue' do
- put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+ put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen'
expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'reopened'
@@ -1137,7 +1240,7 @@ describe API::Issues, api: true do
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
expect(response).to have_http_status(200)
@@ -1147,25 +1250,25 @@ describe API::Issues, api: true do
end
end
- describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
it 'creates a new project issue' do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
- put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date
expect(response).to have_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
- describe "DELETE /projects/:id/issues/:issue_id" do
+ describe "DELETE /projects/:id/issues/:issue_iid" do
it "rejects a non member from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
expect(response).to have_http_status(403)
end
it "rejects a developer from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.id}", author)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
expect(response).to have_http_status(403)
end
@@ -1174,9 +1277,9 @@ describe API::Issues, api: true do
let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
- delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
- expect(response).to have_http_status(200)
- expect(json_response['state']).to eq 'opened'
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
+
+ expect(response).to have_http_status(204)
end
end
@@ -1187,14 +1290,20 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
end
+
+ it 'returns 404 when using the issue ID instead of IID' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(404)
+ end
end
- describe '/projects/:id/issues/:issue_id/move' do
+ describe '/projects/:id/issues/:issue_iid/move' do
let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(201)
@@ -1203,7 +1312,7 @@ describe API::Issues, api: true do
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: project.id
expect(response).to have_http_status(400)
@@ -1213,7 +1322,7 @@ describe API::Issues, api: true do
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project2.id
expect(response).to have_http_status(400)
@@ -1222,13 +1331,23 @@ describe API::Issues, api: true do
end
it 'moves the issue to another namespace if I am admin' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
to_project_id: target_project2.id
expect(response).to have_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
+ context 'when using the issue ID instead of iid' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
context 'when issue does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/123/move", user),
@@ -1241,7 +1360,7 @@ describe API::Issues, api: true do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
- post api("/projects/123/issues/#{issue.id}/move", user),
+ post api("/projects/123/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_http_status(404)
@@ -1251,7 +1370,7 @@ describe API::Issues, api: true do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: 123
expect(response).to have_http_status(404)
@@ -1259,16 +1378,16 @@ describe API::Issues, api: true do
end
end
- describe 'POST :id/issues/:issue_id/subscribe' do
+ describe 'POST :id/issues/:issue_iid/subscribe' do
it 'subscribes to an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
@@ -1279,8 +1398,14 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
+ it 'returns 404 if the issue ID is used instead of the iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member)
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
expect(response).to have_http_status(404)
end
@@ -1288,14 +1413,14 @@ describe API::Issues, api: true do
describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
expect(response).to have_http_status(304)
end
@@ -1306,8 +1431,14 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
+ it 'returns 404 if using the issue ID instead of iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member)
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
new file mode 100644
index 00000000000..9450701064b
--- /dev/null
+++ b/spec/requests/api/jobs_spec.rb
@@ -0,0 +1,480 @@
+require 'spec_helper'
+
+describe API::Jobs, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:api_user) { user }
+ let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
+ let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe 'GET /projects/:id/jobs' do
+ let(:query) { Hash.new }
+
+ before do
+ get api("/projects/#{project.id}/jobs", api_user), query
+ end
+
+ context 'authorized user' do
+ it 'returns project jobs' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
+ context 'filter project with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter project with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_http_status(400) }
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return project builds' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
+ let(:query) { Hash.new }
+
+ before do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline jobs' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
+ context 'filter jobs with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter jobs with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_http_status(400) }
+ end
+
+ context 'jobs in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:build2) { create(:ci_build, pipeline: pipeline2) }
+
+ it 'excludes jobs from other pipelines' do
+ json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return jobs' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id' do
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job data' do
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('test')
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job data' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id/artifacts' do
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user)
+ end
+
+ context 'job with artifacts' do
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ context 'authorized user' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'returns specific job artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ it 'does not return job artifacts if not uploaded' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:api_user) { reporter.user }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def get_for_ref(ref = pipeline.ref, job = build.name)
+ get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job
+ end
+
+ context 'when not logged in' do
+ let(:api_user) { nil }
+
+ before do
+ get_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when logging as guest' do
+ let(:api_user) { guest.user }
+
+ before do
+ get_for_ref
+ end
+
+ it 'gives 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'non-existing job' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get_for_ref('TAIL')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such job' do
+ before do
+ get_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper job' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id/trace' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job trace' do
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq(build.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job trace' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/cancel' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build persmission' do
+ it 'cancels running or pending job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/retry' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build permission' do
+ it 'retries non-running job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/erase' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/erase", user)
+ end
+
+ context 'job is erasable' do
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+
+ it 'erases job content' do
+ expect(response).to have_http_status(201)
+ expect(build.trace).to be_empty
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'updates job' do
+ build.reload
+ expect(build.erased_at).to be_truthy
+ expect(build.erased_by).to eq(user)
+ end
+ end
+
+ context 'job is not erasable' do
+ let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+
+ it 'responds with forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response).to have_http_status(200)
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/play' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/play", user)
+ end
+
+ context 'on an playable job' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the job' do
+ expect(response).to have_http_status(200)
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable job' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Job")
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 566d11bba57..a1adaba7b98 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -21,11 +21,11 @@ describe API::Labels, api: true do
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
- expected_keys = [
- 'id', 'name', 'color', 'description',
- 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
- 'subscribed', 'priority'
- ]
+ expected_keys = %w(
+ id name color description
+ open_issues_count closed_issues_count open_merge_requests_count
+ subscribed priority
+ )
get api("/projects/#{project.id}/labels", user)
@@ -175,9 +175,10 @@ describe API::Labels, api: true do
end
describe 'DELETE /projects/:id/labels' do
- it 'returns 200 for existing label' do
+ it 'returns 204 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
end
it 'returns 404 for non existing label' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 31166b50033..2d37d026a39 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -173,11 +173,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
- it 'returns 422 when access_level is not valid' do
+ it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end
end
end
@@ -230,11 +230,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
- it 'returns 422 when access level is not valid' do
+ it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end
end
end
@@ -263,18 +263,18 @@ describe API::Members, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.members.count }.by(-1)
end
end
context 'when authenticated as a master/owner' do
context 'and member is a requester' do
- it "returns #{source_type == 'project' ? 200 : 404}" do
+ it 'returns 404' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_http_status(404)
end.not_to change { source.requesters.count }
end
end
@@ -283,15 +283,15 @@ describe API::Members, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.members.count }.by(-1)
end
end
- it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ it 'returns 404 if member does not exist' do
delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -342,7 +342,7 @@ describe API::Members, api: true do
post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end.to change { project.members.count }.by(0)
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 1d02e827183..79f3151ba52 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -13,9 +13,9 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
project.team << [user, :master]
end
- describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
it 'returns 200 for a valid merge request' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user)
merge_request_diff = merge_request.merge_request_diffs.first
expect(response.status).to eq 200
@@ -26,16 +26,22 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
end
- it 'returns a 404 when merge_request_id not found' do
+ it 'returns a 404 when merge_request id is used instead of the iid' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/versions", user)
expect(response).to have_http_status(404)
end
end
- describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
it 'returns a 200 for a valid merge request' do
- merge_request_diff = merge_request.merge_request_diffs.first
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}", user)
expect(response.status).to eq 200
expect(json_response['id']).to eq(merge_request_diff.id)
@@ -43,8 +49,18 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
end
- it 'returns a 404 when merge_request_id not found' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+ it 'returns a 404 when merge_request id is used instead of the iid' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 when merge_request version_id is not found' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 when merge_request_iid is not found' do
+ get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user)
expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index c125df8b90b..9aba1d75612 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -93,6 +93,13 @@ describe API::MergeRequests, api: true do
expect(json_response.first['id']).to eq merge_request_closed.id
end
+ it 'matches V4 response schema' do
+ get api("/projects/#{project.id}/merge_requests", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_requests')
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -146,9 +153,9 @@ describe API::MergeRequests, api: true do
end
end
- describe "GET /projects/:id/merge_requests/:merge_request_id" do
+ describe "GET /projects/:id/merge_requests/:merge_request_iid" do
it 'exposes known attributes' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
@@ -170,14 +177,14 @@ describe API::MergeRequests, api: true do
expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
expect(json_response['work_in_progress']).to be_falsy
- expect(json_response['merge_when_build_succeeds']).to be_falsy
+ expect(json_response['merge_when_pipeline_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
end
it "returns merge_request" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
@@ -187,25 +194,31 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
- it "returns a 404 error if merge_request_id not found" do
+ it "returns a 404 error if merge_request_iid not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
end
+ it "returns a 404 error if merge_request `id` is used instead of iid" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(404)
+ end
+
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
it "returns merge_request" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
expect(response).to have_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
end
- describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
it 'returns a 200 when merge request is valid' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
commit = merge_request.commits.first
expect(response.status).to eq 200
@@ -216,24 +229,36 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(commit.title)
end
- it 'returns a 404 when merge_request_id not found' do
+ it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/commits", user)
expect(response).to have_http_status(404)
end
+
+ it 'returns a 404 when merge_request id is used instead of iid' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+
+ expect(response).to have_http_status(404)
+ end
end
- describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do
it 'returns the change information of the merge_request' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
end
- it 'returns a 404 when merge_request_id not found' do
+ it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/changes", user)
expect(response).to have_http_status(404)
end
+
+ it 'returns a 404 when merge_request id is used instead of iid' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+
+ expect(response).to have_http_status(404)
+ end
end
describe "POST /projects/:id/merge_requests" do
@@ -250,7 +275,7 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['force_remove_source_branch']).to be_truthy
end
@@ -393,7 +418,7 @@ describe API::MergeRequests, api: true do
end
end
- describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ describe "DELETE /projects/:id/merge_requests/:merge_request_iid" do
context "when the user is developer" do
let(:developer) { create(:user) }
@@ -402,25 +427,37 @@ describe API::MergeRequests, api: true do
end
it "denies the deletion of the merge request" do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer)
expect(response).to have_http_status(403)
end
end
context "when the user is project owner" do
it "destroys the merge request owners can destroy" do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_http_status(204)
+ end
+
+ it "returns 404 for an invalid merge request IID" do
+ delete api("/projects/#{project.id}/merge_requests/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 if the merge request id is used instead of iid" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(404)
end
end
end
- describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+ describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
it "returns merge_request in case of success" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(200)
end
@@ -429,7 +466,7 @@ describe API::MergeRequests, api: true do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
@@ -437,14 +474,14 @@ describe API::MergeRequests, api: true do
it "returns 405 if merge_request is not open" do
merge_request.close
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -452,7 +489,7 @@ describe API::MergeRequests, api: true do
it 'returns 405 if the build failed for a merge request that requires success' do
allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
@@ -461,20 +498,20 @@ describe API::MergeRequests, api: true do
it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2)
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
it "returns 409 if the SHA parameter doesn't match" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse
expect(response).to have_http_status(409)
expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
end
it "succeeds if the SHA parameter matches" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha
expect(response).to have_http_status(200)
end
@@ -483,18 +520,30 @@ describe API::MergeRequests, api: true do
allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
allow(pipeline).to receive(:active?).and_return(true)
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('Test')
- expect(json_response['merge_when_build_succeeds']).to eq(true)
+ expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
+ end
+
+ it "returns 404 for an invalid merge request IID" do
+ put api("/projects/#{project.id}/merge_requests/12345/merge", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 if the merge request id is used instead of iid" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(404)
end
end
- describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
context "to close a MR" do
it "returns merge_request" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close"
expect(response).to have_http_status(200)
expect(json_response['state']).to eq('closed')
@@ -502,38 +551,38 @@ describe API::MergeRequests, api: true do
end
it "updates title and returns merge_request" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title"
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('New title')
end
it "updates description and returns merge_request" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description"
expect(response).to have_http_status(200)
expect(json_response['description']).to eq('New description')
end
it "updates milestone_id and returns merge_request" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id
expect(response).to have_http_status(200)
expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "returns merge_request with renamed target_branch" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
it "returns merge_request that removes the source branch" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true
expect(response).to have_http_status(200)
expect(json_response['force_remove_source_branch']).to be_truthy
end
it 'allows special label names' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
@@ -546,7 +595,7 @@ describe API::MergeRequests, api: true do
end
it 'does not update state when title is empty' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil
merge_request.reload
expect(response).to have_http_status(400)
@@ -554,19 +603,31 @@ describe API::MergeRequests, api: true do
end
it 'does not update state when target_branch is empty' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil
merge_request.reload
expect(response).to have_http_status(400)
expect(merge_request.state).to eq('opened')
end
+
+ it "returns 404 for an invalid merge request IID" do
+ put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close"
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 if the merge request id is used instead of iid" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+ expect(response).to have_http_status(404)
+ end
end
- describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+ describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do
it "returns comment" do
original_count = merge_request.notes.size
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment"
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
@@ -576,23 +637,29 @@ describe API::MergeRequests, api: true do
end
it "returns 400 if note is missing" do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
expect(response).to have_http_status(400)
end
- it "returns 404 if note is attached to non existent merge request" do
+ it "returns 404 if merge request iid is invalid" do
post api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
expect(response).to have_http_status(404)
end
+
+ it "returns 404 if merge request id is used instead of iid" do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user),
+ note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
end
- describe "GET :id/merge_requests/:merge_request_id/comments" do
+ describe "GET :id/merge_requests/:merge_request_iid/comments" do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
it "returns merge_request comments ordered by created_at" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -603,20 +670,25 @@ describe API::MergeRequests, api: true do
expect(json_response.last['note']).to eq("another comment on a MR")
end
- it "returns a 404 error if merge_request_id not found" do
+ it "returns a 404 error if merge_request_iid is invalid" do
get api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response).to have_http_status(404)
end
+
+ it "returns a 404 error if merge_request id is used instead of iid" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(404)
+ end
end
- describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+ describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do
it 'returns the issue that will be closed on merge' do
issue = create(:issue, project: project)
mr = merge_request.tap do |mr|
mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
end
- get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -626,7 +698,7 @@ describe API::MergeRequests, api: true do
end
it 'returns an empty array when there are no issues to be closed' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -640,7 +712,7 @@ describe API::MergeRequests, api: true do
merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
- get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -656,22 +728,34 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest)
expect(response).to have_http_status(403)
end
+
+ it "returns 404 for an invalid merge request IID" do
+ get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 if the merge request id is used instead of iid" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response).to have_http_status(404)
+ end
end
- describe 'POST :id/merge_requests/:merge_request_id/subscribe' do
+ describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do
it 'subscribes to a merge request' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user)
expect(response).to have_http_status(304)
end
@@ -682,26 +766,32 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+ it 'returns 404 if the merge request id is used instead of iid' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
expect(response).to have_http_status(403)
end
end
- describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do
+ describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do
it 'unsubscribes from a merge request' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
expect(response).to have_http_status(304)
end
@@ -712,11 +802,17 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+ it 'returns 404 if the merge request id is used instead of iid' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
+
+ expect(response).to have_http_status(404)
+ end
+
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest)
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 418bf5a507c..7fb728fed6f 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -4,8 +4,8 @@ describe API::Milestones, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:empty_project, namespace: user.namespace ) }
- let!(:closed_milestone) { create(:closed_milestone, project: project) }
- let!(:milestone) { create(:milestone, project: project) }
+ let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
+ let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
before { project.team << [user, :developer] }
@@ -45,8 +45,37 @@ describe API::Milestones, api: true do
expect(json_response.first['id']).to eq(closed_milestone.id)
end
- it 'returns a project milestone by iid' do
- get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
+ it 'returns an array of milestones specified by iids' do
+ other_milestone = create(:milestone, project: project)
+
+ get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
+ end
+
+ it 'does not return any milestone if none found' do
+ get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+ end
+
+ describe 'GET /projects/:id/milestones/:milestone_id' do
+ it 'returns a project milestone by id' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(milestone.title)
+ expect(json_response['iid']).to eq(milestone.iid)
+ end
+
+ it 'returns a project milestone by iids array' do
+ get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
@@ -56,21 +85,22 @@ describe API::Milestones, api: true do
expect(json_response.first['id']).to eq closed_milestone.id
end
- it 'returns a project milestone by iid array' do
- get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+ it 'returns a project milestone by searching for title' do
+ get api("/projects/#{project.id}/milestones", user), search: 'version2'
expect(response).to have_http_status(200)
- expect(json_response.size).to eq(2)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq milestone.title
expect(json_response.first['id']).to eq milestone.id
end
- it 'returns a project milestone by iid array' do
- get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+ it 'returns a project milestones by searching for description' do
+ get api("/projects/#{project.id}/milestones", user), search: 'open'
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
- expect(json_response.size).to eq(2)
+ expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq milestone.title
expect(json_response.first['id']).to eq milestone.id
end
@@ -197,6 +227,13 @@ describe API::Milestones, api: true do
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
+ it 'matches V4 response schema for a list of issues' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ end
+
it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
@@ -206,8 +243,8 @@ describe API::Milestones, api: true do
describe 'confidential issues' do
let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) }
- let(:issue) { create(:issue, project: public_project) }
- let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+ let(:issue) { create(:issue, project: public_project, position: 2) }
+ let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) }
before do
public_project.team << [user, :developer]
@@ -246,11 +283,24 @@ describe API::Milestones, api: true do
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
end
+
+ it 'returns issues ordered by position asc' do
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ expect(json_response.second['id']).to eq(issue.id)
+ end
end
end
describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project, position: 2) }
+ let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) }
+
before do
milestone.merge_requests << merge_request
end
@@ -283,5 +333,18 @@ describe API::Milestones, api: true do
expect(response).to have_http_status(401)
end
+
+ it 'returns merge_requests ordered by position asc' do
+ milestone.merge_requests << another_merge_request
+
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['id']).to eq(another_merge_request.id)
+ expect(json_response.second['id']).to eq(merge_request.id)
+ end
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 3cca4468be7..347f8f6fa3b 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -225,11 +225,11 @@ describe API::Notes, api: true do
context 'when the user is posting an award emoji on an issue created by someone else' do
let(:issue2) { create(:issue, project: project) }
- it 'returns an award emoji' do
+ it 'creates a new issue note' do
post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
expect(response).to have_http_status(201)
- expect(json_response['awardable_id']).to eq issue2.id
+ expect(json_response['body']).to eq(':+1:')
end
end
@@ -373,7 +373,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
@@ -392,7 +392,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
@@ -412,7 +412,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 7e2cc50e591..367225df717 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -29,5 +29,27 @@ describe API::API, api: true do
expect(json_response['access_token']).not_to be_nil
end
end
+
+ context "when user is blocked" do
+ it "does not create an access token" do
+ user = create(:user)
+ user.block
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when user is ldap_blocked" do
+ it "does not create an access token" do
+ user = create(:user)
+ user.ldap_block
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 98d004b572e..51af999b455 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -24,6 +24,7 @@ describe API::Pipelines, api: true do
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id
+ expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status])
end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 20c76bd2c05..b1f8c249092 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -33,7 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
- expect(json_response.first['build_events']).to eq(true)
+ expect(json_response.first['job_events']).to eq(true)
expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['wiki_page_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
@@ -59,7 +59,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['job_events']).to eq(hook.build_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -98,7 +98,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
- expect(json_response['build_events']).to eq(false)
+ expect(json_response['job_events']).to eq(false)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true)
@@ -144,7 +144,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['job_events']).to eq(hook.build_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -183,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "deletes hook from project" do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- end.to change {project.hooks.count}.by(-1)
- expect(response).to have_http_status(200)
- end
- it "returns success when deleting hook" do
- delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change {project.hooks.count}.by(-1)
end
it "returns a 404 error when deleting non existent hook" do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index da9df56401b..9e88c19b0bc 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -44,7 +44,7 @@ describe API::ProjectSnippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
code: 'puts "hello world"',
- visibility_level: Snippet::PUBLIC
+ visibility: 'public'
}
end
@@ -56,7 +56,7 @@ describe API::ProjectSnippets, api: true do
expect(snippet.content).to eq(params[:code])
expect(snippet.title).to eq(params[:title])
expect(snippet.file_name).to eq(params[:file_name])
- expect(snippet.visibility_level).to eq(params[:visibility_level])
+ expect(snippet.visibility_level).to eq(Snippet::PUBLIC)
end
it 'returns 400 for missing parameters' do
@@ -80,14 +80,14 @@ describe API::ProjectSnippets, api: true do
context 'when the snippet is private' do
it 'creates the snippet' do
- expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ expect { create_snippet(project, visibility: 'private') }.
to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
- it 'rejects the shippet' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ it 'rejects the snippet' do
+ expect { create_snippet(project, visibility: 'public') }.
not_to change { Snippet.count }
expect(response).to have_http_status(400)
@@ -95,7 +95,7 @@ describe API::ProjectSnippets, api: true do
end
it 'creates a spam log' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ expect { create_snippet(project, visibility: 'public') }.
to change { SpamLog.count }.by(1)
end
end
@@ -165,7 +165,7 @@ describe API::ProjectSnippets, api: true do
let(:visibility_level) { Snippet::PRIVATE }
it 'rejects the snippet' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
not_to change { snippet.reload.title }
expect(response).to have_http_status(400)
@@ -173,7 +173,7 @@ describe API::ProjectSnippets, api: true do
end
it 'creates a spam log' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
to change { SpamLog.count }.by(1)
end
end
@@ -189,7 +189,7 @@ describe API::ProjectSnippets, api: true do
delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it 'returns 404 for invalid snippet id' do
@@ -212,7 +212,7 @@ describe API::ProjectSnippets, api: true do
end
it 'returns 404 for invalid snippet id' do
- delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
+ get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 4e90aae9279..c481b7e72b1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
require 'spec_helper'
-describe API::Projects, api: true do
- include ApiHelpers
+describe API::Projects, :api do
include Gitlab::CurrentSettings
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -43,9 +43,10 @@ describe API::Projects, api: true do
describe 'GET /projects' do
shared_examples_for 'projects response' do
it 'returns an array of projects' do
- get api('/projects', current_user)
+ get api('/projects', current_user), filter
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
end
@@ -61,6 +62,7 @@ describe API::Projects, api: true do
context 'when unauthenticated' do
it_behaves_like 'projects response' do
+ let(:filter) { {} }
let(:current_user) { nil }
let(:projects) { [public_project] }
end
@@ -68,6 +70,7 @@ describe API::Projects, api: true do
context 'when authenticated as regular user' do
it_behaves_like 'projects response' do
+ let(:filter) { {} }
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3] }
end
@@ -121,7 +124,7 @@ describe API::Projects, api: true do
context 'and with simple=true' do
it 'returns a simplified version of all the projects' do
- expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+ expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
get api('/projects?simple=true', user)
@@ -133,13 +136,18 @@ describe API::Projects, api: true do
end
context 'and using search' do
- it 'returns searched project' do
- get api('/projects', user), { search: project.name }
+ it_behaves_like 'projects response' do
+ let(:filter) { { search: project.name } }
+ let(:current_user) { user }
+ let(:projects) { [project] }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ context 'and membership=true' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { membership: true } }
+ let(:current_user) { user }
+ let(:projects) { [project, project2, project3] }
end
end
@@ -216,36 +224,52 @@ describe API::Projects, api: true do
end
context 'and with all query parameters' do
- # | | project5 | project6 | project7 | project8 | project9 |
- # |---------+----------+----------+----------+----------+----------|
- # | search | x | | x | x | x |
- # | starred | x | x | | x | x |
- # | public | x | x | x | | x |
- # | owned | x | x | x | x | |
- let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) }
+ let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: create(:namespace)) }
let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) }
let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) }
let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) }
let!(:project9) { create(:empty_project, :public, path: 'gitlab9') }
before do
- user.update_attributes(starred_projects: [project5, project6, project8, project9])
+ user.update_attributes(starred_projects: [project5, project7, project8, project9])
end
- it 'returns only projects that satify all query parameters' do
- get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
+ context 'including owned filter' do
+ it 'returns only projects that satisfy all query parameters' do
+ get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(project5.id)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(project7.id)
+ end
+ end
+
+ context 'including membership filter' do
+ before do
+ create(:project_member,
+ user: user,
+ project: project5,
+ access_level: ProjectMember::MASTER)
+ end
+
+ it 'returns only projects that satisfy all query parameters' do
+ get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project5.id, project7.id)
+ end
end
end
end
context 'when authenticated as a different user' do
it_behaves_like 'projects response' do
+ let(:filter) { {} }
let(:current_user) { user2 }
let(:projects) { [public_project] }
end
@@ -253,6 +277,7 @@ describe API::Projects, api: true do
context 'when authenticated as admin' do
it_behaves_like 'projects response' do
+ let(:filter) { {} }
let(:current_user) { admin }
let(:projects) { Project.all }
end
@@ -269,10 +294,37 @@ describe API::Projects, api: true do
end
end
- it 'creates new project without path and return 201' do
- expect { post api('/projects', user), name: 'foo' }.
+ it 'creates new project without path but with name and returns 201' do
+ expect { post api('/projects', user), name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project without name but with path and returns 201' do
+ expect { post api('/projects', user), path: 'foo_project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('foo_project')
+ expect(project.path).to eq('foo_project')
+ end
+
+ it 'creates new project name and path and returns 201' do
+ expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-Project')
end
it 'creates last project before reaching project limit' do
@@ -281,7 +333,7 @@ describe API::Projects, api: true do
expect(response).to have_http_status(201)
end
- it 'does not create new project without name and return 400' do
+ it 'does not create new project without name or path and returns 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
@@ -293,7 +345,7 @@ describe API::Projects, api: true do
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
- only_allow_merge_if_build_succeeds: false,
+ only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false
})
@@ -313,36 +365,39 @@ describe API::Projects, api: true do
end
it 'sets a project as public' do
- project = attributes_for(:project, :public)
+ project = attributes_for(:project, visibility: 'public')
+
post api('/projects', user), project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(json_response['visibility']).to eq('public')
end
it 'sets a project as internal' do
- project = attributes_for(:project, :internal)
+ project = attributes_for(:project, visibility: 'internal')
+
post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+
+ expect(json_response['visibility']).to eq('internal')
end
it 'sets a project as private' do
- project = attributes_for(:project, :private)
+ project = attributes_for(:project, visibility: 'private')
+
post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+
+ expect(json_response['visibility']).to eq('private')
end
it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
post api('/projects', user), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
- it 'sets a project as allowing merge only if build succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
post api('/projects', user), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
@@ -369,8 +424,16 @@ describe API::Projects, api: true do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
+ it 'ignores import_url when it is nil' do
+ project = attributes_for(:project, { import_url: nil })
+
+ post api('/projects', user), project
+
+ expect(response).to have_http_status(201)
+ end
+
context 'when a visibility level is restricted' do
- let(:project_param) { attributes_for(:project, :public) }
+ let(:project_param) { attributes_for(:project, visibility: 'public') }
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -388,10 +451,7 @@ describe API::Projects, api: true do
it 'allows an admin to override restricted visibility settings' do
post api('/projects', admin), project_param
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to(
- eq(Gitlab::VisibilityLevel::PUBLIC)
- )
+ expect(json_response['visibility']).to eq('public')
end
end
end
@@ -432,40 +492,41 @@ describe API::Projects, api: true do
end
it 'sets a project as public' do
- project = attributes_for(:project, :public)
+ project = attributes_for(:project, visibility: 'public')
+
post api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(json_response['visibility']).to eq('public')
end
it 'sets a project as internal' do
- project = attributes_for(:project, :internal)
+ project = attributes_for(:project, visibility: 'internal')
+
post api("/projects/user/#{user.id}", admin), project
expect(response).to have_http_status(201)
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ expect(json_response['visibility']).to eq('internal')
end
it 'sets a project as private' do
- project = attributes_for(:project, :private)
+ project = attributes_for(:project, visibility: 'private')
+
post api("/projects/user/#{user.id}", admin), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+
+ expect(json_response['visibility']).to eq('private')
end
it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
post api("/projects/user/#{user.id}", admin), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
- it 'sets a project as allowing merge only if build succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
post api("/projects/user/#{user.id}", admin), project
- expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
@@ -529,9 +590,8 @@ describe API::Projects, api: true do
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
expect(json_response['tag_list']).to be_an Array
- expect(json_response['public']).to be_falsey
expect(json_response['archived']).to be_falsey
- expect(json_response['visibility_level']).to be_present
+ expect(json_response['visibility']).to be_present
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
@@ -542,7 +602,7 @@ describe API::Projects, api: true do
expect(json_response['issues_enabled']).to be_present
expect(json_response['merge_requests_enabled']).to be_present
expect(json_response['wiki_enabled']).to be_present
- expect(json_response['builds_enabled']).to be_present
+ expect(json_response['jobs_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
@@ -553,13 +613,13 @@ describe API::Projects, api: true do
expect(json_response['avatar_url']).to be_nil
expect(json_response['star_count']).to be_present
expect(json_response['forks_count']).to be_present
- expect(json_response['public_builds']).to be_present
+ expect(json_response['public_jobs']).to be_present
expect(json_response['shared_with_groups']).to be_an Array
expect(json_response['shared_with_groups'].length).to eq(1)
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
- expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
end
@@ -785,8 +845,7 @@ describe API::Projects, api: true do
describe 'POST /projects/:id/snippets' do
it 'creates a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
- title: 'api test', file_name: 'sample.rb', code: 'test',
- visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('api test')
end
@@ -820,8 +879,9 @@ describe API::Projects, api: true do
it 'deletes existing project snippet' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change { Snippet.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 when deleting unknown snippet id' do
@@ -905,8 +965,10 @@ describe API::Projects, api: true do
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
+
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
@@ -1035,7 +1097,7 @@ describe API::Projects, api: true do
end
it 'updates visibility_level' do
- project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+ project_param = { visibility: 'public' }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
@@ -1045,13 +1107,13 @@ describe API::Projects, api: true do
it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
- project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+ project_param = { visibility: 'private' }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(json_response['visibility']).to eq('private')
end
it 'does not update name to existing name' do
@@ -1118,7 +1180,7 @@ describe API::Projects, api: true do
end
it 'does not update visibility_level' do
- project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+ project_param = { visibility: 'public' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
@@ -1263,7 +1325,9 @@ describe API::Projects, api: true do
context 'when authenticated as user' do
it 'removes project' do
delete api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a project if not an owner' do
@@ -1287,7 +1351,9 @@ describe API::Projects, api: true do
context 'when authenticated as admin' do
it 'removes any existing project' do
delete api("/projects/#{project.id}", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a non existing project' do
@@ -1422,4 +1488,53 @@ describe API::Projects, api: true do
end
end
end
+
+ describe 'POST /projects/:id/housekeeping' do
+ let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+ before do
+ allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping)
+ end
+
+ context 'when authenticated as owner' do
+ it 'starts the housekeeping process' do
+ expect(housekeeping).to receive(:execute).once
+
+ post api("/projects/#{project.id}/housekeeping", user)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when housekeeping lease is taken' do
+ it 'returns conflict' do
+ expect(housekeeping).to receive(:execute).once.and_raise(Projects::HousekeepingService::LeaseTaken)
+
+ post api("/projects/#{project.id}/housekeeping", user)
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/)
+ end
+ end
+ end
+
+ context 'when authenticated as developer' do
+ before do
+ project_member2
+ end
+
+ it 'returns forbidden error' do
+ post api("/projects/#{project.id}/housekeeping", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/projects/#{project.id}/housekeeping")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 7652606a491..4783d011d54 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -30,7 +30,7 @@ describe API::Repositories, api: true do
context 'when ref does not exist' do
it_behaves_like '404 response' do
- let(:request) { get api("#{route}?ref_name=foo", current_user) }
+ let(:request) { get api("#{route}?ref=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end
@@ -66,7 +66,7 @@ describe API::Repositories, api: true do
context 'when ref does not exist' do
it_behaves_like '404 response' do
- let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) }
+ let(:request) { get api("#{route}?recursive=1&ref=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end
@@ -100,82 +100,70 @@ describe API::Repositories, api: true do
end
end
- {
- 'blobs/:sha' => 'blobs/master',
- 'commits/:sha/blob' => 'commits/master/blob'
- }.each do |desc_path, example_path|
- describe "GET /projects/:id/repository/#{desc_path}" do
- let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+ describe "GET /projects/:id/repository/blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" }
- shared_examples_for 'repository blob' do
- it 'returns the repository blob' do
- get api(route, current_user)
-
- expect(response).to have_http_status(200)
- end
-
- context 'when sha does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) }
- let(:message) { '404 Commit Not Found' }
- end
- end
+ shared_examples_for 'repository blob' do
+ it 'returns blob attributes as json' do
+ get api(route, current_user)
- context 'when filepath does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) }
- let(:message) { '404 File Not Found' }
- end
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['size']).to eq(111)
+ expect(json_response['encoding']).to eq("base64")
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+ expect(json_response['sha']).to eq(sample_blob.oid)
+ end
- context 'when no filepath is given' do
- it_behaves_like '400 response' do
- let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) }
- end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) }
+ let(:message) { '404 Blob Not Found' }
end
+ end
- context 'when repository is disabled' do
- include_context 'disabled repository'
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- it_behaves_like '403 response' do
- let(:request) { get api(route, current_user) }
- end
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
end
end
+ end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository blob' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
end
+ end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route) }
- let(:message) { '404 Project Not Found' }
- end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
+ end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository blob' do
- let(:current_user) { user }
- end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository blob' do
+ let(:current_user) { user }
end
+ end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route, guest) }
- end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
end
end
end
- describe "GET /projects/:id/repository/raw_blobs/:sha" do
- let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+ describe "GET /projects/:id/repository/blobs/:sha/raw" do
+ let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}/raw" }
shared_examples_for 'repository raw blob' do
it 'returns the repository raw blob' do
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
get api(route, current_user)
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
new file mode 100644
index 00000000000..044b989e5ba
--- /dev/null
+++ b/spec/requests/api/runner_spec.rb
@@ -0,0 +1,1072 @@
+require 'spec_helper'
+
+describe API::Runner do
+ include ApiHelpers
+ include StubGitlabCalls
+
+ let(:registration_token) { 'abcdefg123456' }
+
+ before do
+ stub_gitlab_calls
+ stub_application_setting(runners_registration_token: registration_token)
+ end
+
+ describe '/api/v4/runners' do
+ describe 'POST /api/v4/runners' do
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/runners')
+
+ expect(response).to have_http_status 400
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/runners'), token: 'invalid'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ it 'creates runner with default values' do
+ post api('/runners'), token: registration_token
+
+ runner = Ci::Runner.first
+
+ expect(response).to have_http_status 201
+ expect(json_response['id']).to eq(runner.id)
+ expect(json_response['token']).to eq(runner.token)
+ expect(runner.run_untagged).to be true
+ expect(runner.token).not_to eq(registration_token)
+ end
+
+ context 'when project token is used' do
+ let(:project) { create(:empty_project) }
+
+ it 'creates runner' do
+ post api('/runners'), token: project.runners_token
+
+ expect(response).to have_http_status 201
+ expect(project.runners.size).to eq(1)
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
+ expect(Ci::Runner.first.token).not_to eq(project.runners_token)
+ end
+ end
+ end
+
+ context 'when runner description is provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ description: 'server.hostname'
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.description).to eq('server.hostname')
+ end
+ end
+
+ context 'when runner tags are provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ tag_list: 'tag1, tag2'
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
+ end
+ end
+
+ context 'when option for running untagged jobs is provided' do
+ context 'when tags are provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ run_untagged: false,
+ tag_list: ['tag']
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be false
+ expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
+ end
+ end
+
+ context 'when tags are not provided' do
+ it 'returns 404 error' do
+ post api('/runners'), token: registration_token,
+ run_untagged: false
+
+ expect(response).to have_http_status 404
+ end
+ end
+ end
+
+ context 'when option for locking Runner is provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ locked: true
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.locked).to be true
+ end
+ end
+
+ %w(name version revision platform architecture).each do |param|
+ context "when info parameter '#{param}' info is present" do
+ let(:value) { "#{param}_value" }
+
+ it "updates provided Runner's parameter" do
+ post api('/runners'), token: registration_token,
+ info: { param => value }
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /api/v4/runners' do
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ delete api('/runners')
+
+ expect(response).to have_http_status 400
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ delete api('/runners'), token: 'invalid'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'deletes Runner' do
+ delete api('/runners'), token: runner.token
+
+ expect(response).to have_http_status 204
+ expect(Ci::Runner.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'POST /api/v4/runners/verify' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/runners/verify')
+
+ expect(response).to have_http_status :bad_request
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/runners/verify'), token: 'invalid-token'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ it 'verifies Runner credentials' do
+ post api('/runners/verify'), token: runner.token
+
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe '/api/v4/jobs' do
+ let(:project) { create(:empty_project, shared_runners_enabled: false) }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
+ let(:runner) { create(:ci_runner) }
+ let!(:job) do
+ create(:ci_build, :artifacts, :extended_options,
+ pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
+ end
+
+ before { project.runners << runner }
+
+ describe 'POST /api/v4/jobs/request' do
+ let!(:last_update) {}
+ let!(:new_update) { }
+ let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }
+
+ before { stub_container_registry_config(enabled: false) }
+
+ shared_examples 'no jobs available' do
+ before { request_job }
+
+ context 'when runner sends version in User-Agent' do
+ context 'for stable version' do
+ it 'gives 204 and set X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header).to have_key('X-GitLab-Last-Update')
+ end
+ end
+
+ context 'when last_update is up-to-date' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+
+ it 'gives 204 and set the same X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update']).to eq(last_update)
+ end
+ end
+
+ context 'when last_update is outdated' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+ let(:new_update) { runner.tick_runner_queue }
+
+ it 'gives 204 and set a new X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
+ end
+ end
+
+ context 'when beta version is sent' do
+ let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
+
+ it { expect(response).to have_http_status(204) }
+ end
+
+ context 'when pre-9-0 version is sent' do
+ let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
+
+ it { expect(response).to have_http_status(204) }
+ end
+
+ context 'when pre-9-0 beta version is sent' do
+ let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
+
+ it { expect(response).to have_http_status(204) }
+ end
+ end
+ end
+
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/jobs/request')
+
+ expect(response).to have_http_status 400
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/jobs/request'), token: 'invalid'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ context 'when Runner is not active' do
+ let(:runner) { create(:ci_runner, :inactive) }
+
+ it 'returns 204 error' do
+ request_job
+
+ expect(response).to have_http_status 204
+ end
+ end
+
+ context 'when jobs are finished' do
+ before { job.success }
+
+ it_behaves_like 'no jobs available'
+ end
+
+ context 'when other projects have pending jobs' do
+ before do
+ job.success
+ create(:ci_build, :pending)
+ end
+
+ it_behaves_like 'no jobs available'
+ end
+
+ context 'when shared runner requests job for project without shared_runners_enabled' do
+ let(:runner) { create(:ci_runner, :shared) }
+
+ it_behaves_like 'no jobs available'
+ end
+
+ context 'when there is a pending job' do
+ let(:expected_job_info) do
+ { 'name' => job.name,
+ 'stage' => job.stage,
+ 'project_id' => job.project.id,
+ 'project_name' => job.project.name }
+ end
+
+ let(:expected_git_info) do
+ { 'repo_url' => job.repo_url,
+ 'ref' => job.ref,
+ 'sha' => job.sha,
+ 'before_sha' => job.before_sha,
+ 'ref_type' => 'branch' }
+ end
+
+ let(:expected_steps) do
+ [{ 'name' => 'script',
+ 'script' => %w(ls date),
+ 'timeout' => job.timeout,
+ 'when' => 'on_success',
+ 'allow_failure' => false },
+ { 'name' => 'after_script',
+ 'script' => %w(ls date),
+ 'timeout' => job.timeout,
+ 'when' => 'always',
+ 'allow_failure' => true }]
+ end
+
+ let(:expected_variables) do
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
+ { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
+ end
+
+ let(:expected_artifacts) do
+ [{ 'name' => 'artifacts_file',
+ 'untracked' => false,
+ 'paths' => %w(out/),
+ 'when' => 'always',
+ 'expire_in' => '7d' }]
+ end
+
+ let(:expected_cache) do
+ [{ 'key' => 'cache_key',
+ 'untracked' => false,
+ 'paths' => ['vendor/*'] }]
+ end
+
+ it 'picks a job' do
+ request_job info: { platform: :darwin }
+
+ expect(response).to have_http_status(201)
+ expect(response.headers).not_to have_key('X-GitLab-Last-Update')
+ expect(runner.reload.platform).to eq('darwin')
+ expect(json_response['id']).to eq(job.id)
+ expect(json_response['token']).to eq(job.token)
+ expect(json_response['job_info']).to eq(expected_job_info)
+ expect(json_response['git_info']).to eq(expected_git_info)
+ expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' })
+ expect(json_response['services']).to eq([{ 'name' => 'postgres' }])
+ expect(json_response['steps']).to eq(expected_steps)
+ expect(json_response['artifacts']).to eq(expected_artifacts)
+ expect(json_response['cache']).to eq(expected_cache)
+ expect(json_response['variables']).to include(*expected_variables)
+ end
+
+ context 'when job is made for tag' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+
+ it 'sets branch as ref_type' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['git_info']['ref_type']).to eq('tag')
+ end
+ end
+
+ context 'when job is made for branch' do
+ it 'sets tag as ref_type' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['git_info']['ref_type']).to eq('branch')
+ end
+ end
+
+ it 'updates runner info' do
+ expect { request_job }.to change { runner.reload.contacted_at }
+ end
+
+ %w(name version revision platform architecture).each do |param|
+ context "when info parameter '#{param}' is present" do
+ let(:value) { "#{param}_value" }
+
+ it "updates provided Runner's parameter" do
+ request_job info: { param => value }
+
+ expect(response).to have_http_status(201)
+ expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
+ end
+ end
+ end
+
+ context 'when concurrently updating a job' do
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!).
+ and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
+ end
+
+ it 'returns a conflict' do
+ request_job
+
+ expect(response).to have_http_status(409)
+ expect(response.headers).not_to have_key('X-GitLab-Last-Update')
+ end
+ end
+
+ context 'when project and pipeline have multiple jobs' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+
+ before do
+ job.success
+ job2.success
+ end
+
+ it 'returns dependent jobs' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(test_job.id)
+ expect(json_response['dependencies'].count).to eq(2)
+ expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
+ { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+ end
+ end
+
+ context 'when explicit dependencies are defined' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:test_job) do
+ create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+ stage: 'deploy', stage_idx: 1,
+ options: { dependencies: [job2.name] })
+ end
+
+ before do
+ job.success
+ job2.success
+ end
+
+ it 'returns dependent jobs' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(test_job.id)
+ expect(json_response['dependencies'].count).to eq(1)
+ expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
+ end
+ end
+
+ context 'when job has no tags' do
+ before { job.update(tags: []) }
+
+ context 'when runner is allowed to pick untagged jobs' do
+ before { runner.update_column(:run_untagged, true) }
+
+ it 'picks job' do
+ request_job
+
+ expect(response).to have_http_status 201
+ end
+ end
+
+ context 'when runner is not allowed to pick untagged jobs' do
+ before { runner.update_column(:run_untagged, false) }
+
+ it_behaves_like 'no jobs available'
+ end
+ end
+
+ context 'when triggered job is available' do
+ let(:expected_variables) do
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
+ { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
+ { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true },
+ { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false },
+ { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
+ end
+
+ before do
+ trigger = create(:ci_trigger, project: project)
+ create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
+ project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it 'returns variables for triggers' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['variables']).to include(*expected_variables)
+ end
+ end
+
+ describe 'registry credentials support' do
+ let(:registry_url) { 'registry.example.com:5005' }
+ let(:registry_credentials) do
+ { 'type' => 'registry',
+ 'url' => registry_url,
+ 'username' => 'gitlab-ci-token',
+ 'password' => job.token }
+ end
+
+ context 'when registry is enabled' do
+ before { stub_container_registry_config(enabled: true, host_port: registry_url) }
+
+ it 'sends registry credentials key' do
+ request_job
+
+ expect(json_response).to have_key('credentials')
+ expect(json_response['credentials']).to include(registry_credentials)
+ end
+ end
+
+ context 'when registry is disabled' do
+ before { stub_container_registry_config(enabled: false, host_port: registry_url) }
+
+ it 'does not send registry credentials' do
+ request_job
+
+ expect(json_response).to have_key('credentials')
+ expect(json_response['credentials']).not_to include(registry_credentials)
+ end
+ end
+ end
+ end
+
+ def request_job(token = runner.token, **params)
+ new_params = params.merge(token: token, last_update: last_update)
+ post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
+ end
+ end
+ end
+
+ describe 'PUT /api/v4/jobs/:id' do
+ let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
+
+ before { job.run! }
+
+ context 'when status is given' do
+ it 'mark job as succeeded' do
+ update_job(state: 'success')
+
+ expect(job.reload.status).to eq 'success'
+ end
+
+ it 'mark job as failed' do
+ update_job(state: 'failed')
+
+ expect(job.reload.status).to eq 'failed'
+ end
+ end
+
+ context 'when tace is given' do
+ it 'updates a running build' do
+ update_job(trace: 'BUILD TRACE UPDATED')
+
+ expect(response).to have_http_status(200)
+ expect(job.reload.trace).to eq 'BUILD TRACE UPDATED'
+ end
+ end
+
+ context 'when no trace is given' do
+ it 'does not override trace information' do
+ update_job
+
+ expect(job.reload.trace).to eq 'BUILD TRACE'
+ end
+ end
+
+ context 'when job has been erased' do
+ let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+ it 'responds with forbidden' do
+ update_job
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def update_job(token = job.token, **params)
+ new_params = params.merge(token: token)
+ put api("/jobs/#{job.id}"), new_params
+ end
+ end
+
+ describe 'PATCH /api/v4/jobs/:id/trace' do
+ let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) }
+ let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+ let(:update_interval) { 10.seconds.to_i }
+
+ before { initial_patch_the_trace }
+
+ context 'when request is valid' do
+ it 'gets correct response' do
+ expect(response.status).to eq 202
+ expect(job.reload.trace).to eq 'BUILD TRACE appended'
+ expect(response.header).to have_key 'Range'
+ expect(response.header).to have_key 'Job-Status'
+ end
+
+ context 'when job has been updated recently' do
+ it { expect{ patch_the_trace }.not_to change { job.updated_at }}
+
+ it "changes the job's trace" do
+ patch_the_trace
+
+ expect(job.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect{ force_patch_the_trace }.not_to change { job.updated_at }}
+
+ it "doesn't change the build.trace" do
+ force_patch_the_trace
+
+ expect(job.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+
+ context 'when job was not updated recently' do
+ let(:update_interval) { 15.minutes.to_i }
+
+ it { expect { patch_the_trace }.to change { job.updated_at } }
+
+ it 'changes the job.trace' do
+ patch_the_trace
+
+ expect(job.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect { force_patch_the_trace }.to change { job.updated_at } }
+
+ it "doesn't change the job.trace" do
+ force_patch_the_trace
+
+ expect(job.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+
+ context 'when project for the build has been deleted' do
+ let(:job) do
+ create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job|
+ job.project.update(pending_delete: true)
+ end
+ end
+
+ it 'responds with forbidden' do
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'when Runner makes a force-patch' do
+ before do
+ force_patch_the_trace
+ end
+
+ it 'gets correct response' do
+ expect(response.status).to eq 202
+ expect(job.reload.trace).to eq 'BUILD TRACE appended'
+ expect(response.header).to have_key 'Range'
+ expect(response.header).to have_key 'Job-Status'
+ end
+ end
+
+ context 'when content-range start is too big' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+
+ it 'gets 416 error response with range headers' do
+ expect(response.status).to eq 416
+ expect(response.header).to have_key 'Range'
+ expect(response.header['Range']).to eq '0-11'
+ end
+ end
+
+ context 'when content-range start is too small' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+
+ it 'gets 416 error response with range headers' do
+ expect(response.status).to eq 416
+ expect(response.header).to have_key 'Range'
+ expect(response.header['Range']).to eq '0-11'
+ end
+ end
+
+ context 'when Content-Range header is missing' do
+ let(:headers_with_range) { headers }
+
+ it { expect(response.status).to eq 400 }
+ end
+
+ context 'when job has been errased' do
+ let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+ it { expect(response.status).to eq 403 }
+ end
+
+ def patch_the_trace(content = ' appended', request_headers = nil)
+ unless request_headers
+ offset = job.trace_length
+ limit = offset + content.length - 1
+ request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
+ end
+
+ Timecop.travel(job.updated_at + update_interval) do
+ patch api("/jobs/#{job.id}/trace"), content, request_headers
+ job.reload
+ end
+ end
+
+ def initial_patch_the_trace
+ patch_the_trace(' appended', headers_with_range)
+ end
+
+ def force_patch_the_trace
+ 2.times { patch_the_trace('') }
+ end
+ end
+
+ describe 'artifacts' do
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
+ let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
+ let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
+ let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
+
+ before { job.run! }
+
+ describe 'POST /api/v4/jobs/:id/artifacts/authorize' do
+ context 'when using token as parameter' do
+ it 'authorizes posting artifacts to running job' do
+ authorize_artifacts_with_token_in_params
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).not_to be_nil
+ end
+
+ it 'fails to post too large artifact' do
+ stub_application_setting(max_artifacts_size: 0)
+
+ authorize_artifacts_with_token_in_params(filesize: 100)
+
+ expect(response).to have_http_status(413)
+ end
+ end
+
+ context 'when using token as header' do
+ it 'authorizes posting artifacts to running job' do
+ authorize_artifacts_with_token_in_headers
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).not_to be_nil
+ end
+
+ it 'fails to post too large artifact' do
+ stub_application_setting(max_artifacts_size: 0)
+
+ authorize_artifacts_with_token_in_headers(filesize: 100)
+
+ expect(response).to have_http_status(413)
+ end
+ end
+
+ context 'when using runners token' do
+ it 'fails to authorize artifacts posting' do
+ authorize_artifacts(token: job.project.runners_token)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'reject requests that did not go through gitlab-workhorse' do
+ headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ authorize_artifacts
+
+ expect(response).to have_http_status(500)
+ end
+
+ context 'authorization token is invalid' do
+ it 'responds with forbidden' do
+ authorize_artifacts(token: 'invalid', filesize: 100 )
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def authorize_artifacts(params = {}, request_headers = headers)
+ post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers
+ end
+
+ def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers)
+ params = params.merge(token: job.token)
+ authorize_artifacts(params, request_headers)
+ end
+
+ def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token)
+ authorize_artifacts(params, request_headers)
+ end
+ end
+
+ describe 'POST /api/v4/jobs/:id/artifacts' do
+ context 'when artifacts are being stored inside of tmp path' do
+ before do
+ # by configuring this path we allow to pass temp file from any path
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ end
+
+ context 'when job has been erased' do
+ let(:job) { create(:ci_build, erased_at: Time.now) }
+
+ before do
+ upload_artifacts(file_upload, headers_with_token)
+ end
+
+ it 'responds with forbidden' do
+ upload_artifacts(file_upload, headers_with_token)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when job is running' do
+ shared_examples 'successful artifacts upload' do
+ it 'updates successfully' do
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'when uses regular file post' do
+ before { upload_artifacts(file_upload, headers_with_token, false) }
+
+ it_behaves_like 'successful artifacts upload'
+ end
+
+ context 'when uses accelerated file post' do
+ before { upload_artifacts(file_upload, headers_with_token, true) }
+
+ it_behaves_like 'successful artifacts upload'
+ end
+
+ context 'when updates artifact' do
+ before do
+ upload_artifacts(file_upload2, headers_with_token)
+ upload_artifacts(file_upload, headers_with_token)
+ end
+
+ it_behaves_like 'successful artifacts upload'
+ end
+
+ context 'when using runners token' do
+ it 'responds with forbidden' do
+ upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'when artifacts file is too large' do
+ it 'fails to post too large artifact' do
+ stub_application_setting(max_artifacts_size: 0)
+
+ upload_artifacts(file_upload, headers_with_token)
+
+ expect(response).to have_http_status(413)
+ end
+ end
+
+ context 'when artifacts post request does not contain file' do
+ it 'fails to post artifacts without file' do
+ post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'GitLab Workhorse is not configured' do
+ it 'fails to post artifacts without GitLab-Workhorse' do
+ post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {}
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when setting an expire date' do
+ let(:default_artifacts_expire_in) {}
+ let(:post_data) do
+ { 'file.path' => file_upload.path,
+ 'file.name' => file_upload.original_filename,
+ 'expire_in' => expire_in }
+ end
+
+ before do
+ stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in)
+
+ post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
+ end
+
+ context 'when an expire_in is given' do
+ let(:expire_in) { '7 days' }
+
+ it 'updates when specified' do
+ expect(response).to have_http_status(201)
+ expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
+ end
+ end
+
+ context 'when no expire_in is given' do
+ let(:expire_in) { nil }
+
+ it 'ignores if not specified' do
+ expect(response).to have_http_status(201)
+ expect(job.reload.artifacts_expire_at).to be_nil
+ end
+
+ context 'with application default' do
+ context 'when default is 5 days' do
+ let(:default_artifacts_expire_in) { '5 days' }
+
+ it 'sets to application default' do
+ expect(response).to have_http_status(201)
+ expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
+ end
+ end
+
+ context 'when default is 0' do
+ let(:default_artifacts_expire_in) { '0' }
+
+ it 'does not set expire_in' do
+ expect(response).to have_http_status(201)
+ expect(job.reload.artifacts_expire_at).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context 'posts artifacts file and metadata file' do
+ let!(:artifacts) { file_upload }
+ let!(:metadata) { file_upload2 }
+
+ let(:stored_artifacts_file) { job.reload.artifacts_file.file }
+ let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
+ let(:stored_artifacts_size) { job.reload.artifacts_size }
+
+ before do
+ post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
+ end
+
+ context 'when posts data accelerated by workhorse is correct' do
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'metadata.path' => metadata.path,
+ 'metadata.name' => metadata.original_filename }
+ end
+
+ it 'stores artifacts and artifacts metadata' do
+ expect(response).to have_http_status(201)
+ expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+ expect(stored_artifacts_size).to eq(71759)
+ end
+ end
+
+ context 'when there is no artifacts file in post data' do
+ let(:post_data) do
+ { 'metadata' => metadata }
+ end
+
+ it 'is expected to respond with bad request' do
+ expect(response).to have_http_status(400)
+ end
+
+ it 'does not store metadata' do
+ expect(stored_metadata_file).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'when artifacts are being stored outside of tmp path' do
+ before do
+ # by configuring this path we allow to pass file from @tmpdir only
+ # but all temporary files are stored in system tmp directory
+ @tmpdir = Dir.mktmpdir
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ end
+
+ after { FileUtils.remove_entry @tmpdir }
+
+ it' "fails to post artifacts for outside of tmp path"' do
+ upload_artifacts(file_upload, headers_with_token)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ def upload_artifacts(file, headers = {}, accelerated = true)
+ params = if accelerated
+ { 'file.path' => file.path, 'file.name' => file.original_filename }
+ else
+ { 'file' => file }
+ end
+ post api("/jobs/#{job.id}/artifacts"), params, headers
+ end
+ end
+
+ describe 'GET /api/v4/jobs/:id/artifacts' do
+ let(:token) { job.token }
+
+ before { download_artifact }
+
+ context 'when job has artifacts' do
+ let(:job) { create(:ci_build, :artifacts) }
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ context 'when using job token' do
+ it 'download artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include download_headers
+ end
+ end
+
+ context 'when using runnners token' do
+ let(:token) { job.project.runners_token }
+
+ it 'responds with forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'when job does not has artifacts' do
+ it 'responds with not found' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def download_artifact(params = {}, request_headers = headers)
+ params = params.merge(token: token)
+ get api("/jobs/#{job.id}/artifacts"), params, request_headers
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 103d6755888..8a82543a830 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -277,8 +277,9 @@ describe API::Runners, api: true do
it 'deletes runner' do
expect do
delete api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.shared.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
@@ -286,15 +287,17 @@ describe API::Runners, api: true do
it 'deletes unused runner' do
expect do
delete api("/runners/#{unused_specific_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'deletes used runner' do
expect do
delete api("/runners/#{specific_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
@@ -327,8 +330,9 @@ describe API::Runners, api: true do
it 'deletes runner for one owned project' do
expect do
delete api("/runners/#{specific_runner.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
end
@@ -457,8 +461,9 @@ describe API::Runners, api: true do
it "disables project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{ project.runners.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 776dc655650..fd334934ca5 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -55,7 +55,7 @@ describe API::Services, api: true do
it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 794e2b5c04d..28fab2011a5 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -87,5 +87,23 @@ describe API::Session, api: true do
expect(response).to have_http_status(400)
end
end
+
+ context "when user is blocked" do
+ it "returns authentication error" do
+ user.block
+ post api("/session"), email: user.username, password: user.password
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when user is ldap_blocked" do
+ it "returns authentication error" do
+ user.ldap_block
+ post api("/session"), email: user.username, password: user.password
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 91e3c333a02..11b4b718e2c 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -18,6 +18,9 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['koding_url']).to be_nil
expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil
+ expect(json_response['default_project_visibility']).to be_a String
+ expect(json_response['default_snippet_visibility']).to be_a String
+ expect(json_response['default_group_visibility']).to be_a String
end
end
@@ -30,8 +33,16 @@ describe API::Settings, 'Settings', api: true do
it "updates application settings" do
put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
- plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
+ default_projects_limit: 3,
+ signin_enabled: false,
+ repository_storage: 'custom',
+ koding_enabled: true,
+ koding_url: 'http://koding.example.com',
+ plantuml_enabled: true,
+ plantuml_url: 'http://plantuml.example.com',
+ default_snippet_visibility: 'internal',
+ restricted_visibility_levels: ['public'],
+ default_artifacts_expire_in: '2 days'
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
@@ -41,6 +52,9 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['koding_url']).to eq('http://koding.example.com')
expect(json_response['plantuml_enabled']).to be_truthy
expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+ expect(json_response['default_snippet_visibility']).to eq('internal')
+ expect(json_response['restricted_visibility_levels']).to eq(['public'])
+ expect(json_response['default_artifacts_expire_in']).to eq('2 days')
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 41def7cd1d4..5d75b47b3cd 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -74,7 +74,7 @@ describe API::Snippets, api: true do
end
it 'returns 404 for invalid snippet id' do
- delete api("/snippets/1234", user)
+ get api("/snippets/1234/raw", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
@@ -87,7 +87,7 @@ describe API::Snippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
- visibility_level: Snippet::PUBLIC
+ visibility: 'public'
}
end
@@ -120,14 +120,14 @@ describe API::Snippets, api: true do
context 'when the snippet is private' do
it 'creates the snippet' do
- expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ expect { create_snippet(visibility: 'private') }.
to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
it 'rejects the shippet' do
- expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ expect { create_snippet(visibility: 'public') }.
not_to change { Snippet.count }
expect(response).to have_http_status(400)
@@ -135,7 +135,7 @@ describe API::Snippets, api: true do
end
it 'creates a spam log' do
- expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ expect { create_snippet(visibility: 'public') }.
to change { SpamLog.count }.by(1)
end
end
@@ -218,12 +218,12 @@ describe API::Snippets, api: true do
let(:visibility_level) { Snippet::PRIVATE }
it 'rejects the snippet' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
not_to change { snippet.reload.title }
end
it 'creates a spam log' do
- expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ expect { update_snippet(title: 'Foo', visibility: 'public') }.
to change { SpamLog.count }.by(1)
end
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b59da632c00..d1e10f12657 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -91,6 +91,8 @@ describe API::SystemHooks, api: true do
it "deletes a hook" do
expect do
delete api("/hooks/#{hook.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { SystemHook.count }.by(-1)
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 8a4f078182f..b132d033a61 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -137,8 +137,8 @@ describe API::Tags, api: true do
context 'delete tag' do
it 'deletes an existing tag' do
delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
- expect(response).to have_http_status(200)
- expect(json_response['tag_name']).to eq(tag_name)
+
+ expect(response).to have_http_status(204)
end
it 'raises 404 if the tag does not exist' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 8506e8fccde..2c83e119065 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -58,11 +58,11 @@ describe API::Templates, api: true do
expect(json_response['popular']).to be true
expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
- expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
expect(json_response['limitations']).to eq(%w[no-liability])
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
end
@@ -73,7 +73,7 @@ describe API::Templates, api: true do
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
+ expect(json_response.size).to eq(12)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
end
@@ -102,7 +102,7 @@ describe API::Templates, api: true do
let(:license_type) { 'mit' }
it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
it 'replaces placeholder values' do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index f35e963a14b..b789284fa8d 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Todos, api: true do
include ApiHelpers
- let(:project_1) { create(:empty_project) }
+ let(:project_1) { create(:empty_project, :test_repo) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
let(:author_2) { create(:user) }
@@ -11,7 +11,7 @@ describe API::Todos, api: true do
let(:merge_request) { create(:merge_request, source_project: project_1) }
let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
- let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
+ let!(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) }
let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
before do
@@ -163,7 +163,7 @@ describe API::Todos, api: true do
shared_examples 'an issuable' do |issuable_type|
it 'creates a todo on an issuable' do
- post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
expect(response.status).to eq(201)
expect(json_response['project']).to be_a Hash
@@ -180,7 +180,7 @@ describe API::Todos, api: true do
it 'returns 304 there already exist a todo on that issuable' do
create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
- post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
expect(response.status).to eq(304)
end
@@ -195,7 +195,7 @@ describe API::Todos, api: true do
guest = create(:user)
project_1.team << [guest, :guest]
- post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
if issuable_type == 'merge_requests'
expect(response).to have_http_status(403)
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 92dfc2aa277..d93a734f5b6 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -14,7 +14,7 @@ describe API::Triggers do
let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
- describe 'POST /projects/:project_id/trigger' do
+ describe 'POST /projects/:project_id/trigger/pipeline' do
let!(:project2) { create(:project) }
let(:options) do
{
@@ -28,17 +28,20 @@ describe API::Triggers do
context 'Handles errors' do
it 'returns bad request if token is missing' do
- post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+ post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master'
+
expect(response).to have_http_status(400)
end
it 'returns not found if project is not found' do
- post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+ post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master')
+
expect(response).to have_http_status(404)
end
it 'returns unauthorized if token is for different project' do
- post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+ post api("/projects/#{project2.id}/trigger/pipeline"), options.merge(ref: 'master')
+
expect(response).to have_http_status(401)
end
end
@@ -46,25 +49,21 @@ describe API::Triggers do
context 'Have a commit' do
let(:pipeline) { project.pipelines.last }
- it 'creates builds' do
- post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+ it 'creates pipeline' do
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
+
expect(response).to have_http_status(201)
+ expect(json_response).to include('id' => pipeline.id)
pipeline.builds.reload
expect(pipeline.builds.pending.size).to eq(2)
expect(pipeline.builds.size).to eq(5)
end
- it 'creates builds on webhook from other gitlab repository and branch' do
- expect do
- post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- end.to change(project.builds, :count).by(5)
- expect(response).to have_http_status(201)
- end
+ it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
- it 'returns bad request with no builds created if there\'s no commit for that ref' do
- post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('No builds created')
+ expect(json_response['message']).to eq('No pipeline created')
end
context 'Validates variables' do
@@ -73,22 +72,46 @@ describe API::Triggers do
end
it 'validates variables to be a hash' do
- post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master')
+
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
- post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
it 'creates trigger request with variables' do
- post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master')
+
+ expect(response).to have_http_status(201)
+ expect(pipeline.builds.reload.first.trigger_request.variables).to eq(variables)
+ end
+ end
+ end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
expect(response).to have_http_status(201)
- pipeline.builds.reload
- expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
end
end
end
@@ -123,17 +146,17 @@ describe API::Triggers do
end
end
- describe 'GET /projects/:id/triggers/:token' do
+ describe 'GET /projects/:id/triggers/:trigger_id' do
context 'authenticated user with valid permissions' do
it 'returns trigger details' do
- get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+ get api("/projects/#{project.id}/triggers/#{trigger.id}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_a(Hash)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
- get api("/projects/#{project.id}/triggers/abcdef012345", user)
+ get api("/projects/#{project.id}/triggers/-5", user)
expect(response).to have_http_status(404)
end
@@ -141,7 +164,7 @@ describe API::Triggers do
context 'authenticated user with invalid permissions' do
it 'does not return triggers list' do
- get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+ get api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
expect(response).to have_http_status(403)
end
@@ -149,7 +172,7 @@ describe API::Triggers do
context 'unauthenticated user' do
it 'does not return triggers list' do
- get api("/projects/#{project.id}/triggers/#{trigger.token}")
+ get api("/projects/#{project.id}/triggers/#{trigger.id}")
expect(response).to have_http_status(401)
end
@@ -158,19 +181,31 @@ describe API::Triggers do
describe 'POST /projects/:id/triggers' do
context 'authenticated user with valid permissions' do
- it 'creates trigger' do
- expect do
+ context 'with required parameters' do
+ it 'creates trigger' do
+ expect do
+ post api("/projects/#{project.id}/triggers", user),
+ description: 'trigger'
+ end.to change{project.triggers.count}.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to include('description' => 'trigger')
+ end
+ end
+
+ context 'without required parameters' do
+ it 'does not create trigger' do
post api("/projects/#{project.id}/triggers", user)
- end.to change{project.triggers.count}.by(1)
- expect(response).to have_http_status(201)
- expect(json_response).to be_a(Hash)
+ expect(response).to have_http_status(:bad_request)
+ end
end
end
context 'authenticated user with invalid permissions' do
it 'does not create trigger' do
- post api("/projects/#{project.id}/triggers", user2)
+ post api("/projects/#{project.id}/triggers", user2),
+ description: 'trigger'
expect(response).to have_http_status(403)
end
@@ -178,24 +213,87 @@ describe API::Triggers do
context 'unauthenticated user' do
it 'does not create trigger' do
- post api("/projects/#{project.id}/triggers")
+ post api("/projects/#{project.id}/triggers"),
+ description: 'trigger'
expect(response).to have_http_status(401)
end
end
end
- describe 'DELETE /projects/:id/triggers/:token' do
+ describe 'PUT /projects/:id/triggers/:trigger_id' do
+ context 'authenticated user with valid permissions' do
+ let(:new_description) { 'new description' }
+
+ it 'updates description' do
+ put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
+ description: new_description
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to include('description' => new_description)
+ expect(trigger.reload.description).to eq(new_description)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update trigger' do
+ put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update trigger' do
+ put api("/projects/#{project.id}/triggers/#{trigger.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do
+ context 'authenticated user with valid permissions' do
+ it 'updates owner' do
+ expect(trigger.owner).to be_nil
+
+ post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to include('owner')
+ expect(trigger.reload.owner).to eq(user)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/triggers/:trigger_id' do
context 'authenticated user with valid permissions' do
it 'deletes trigger' do
expect do
- delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+ delete api("/projects/#{project.id}/triggers/#{trigger.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{project.triggers.count}.by(-1)
- expect(response).to have_http_status(200)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
- delete api("/projects/#{project.id}/triggers/abcdef012345", user)
+ delete api("/projects/#{project.id}/triggers/-5", user)
expect(response).to have_http_status(404)
end
@@ -203,7 +301,7 @@ describe API::Triggers do
context 'authenticated user with invalid permissions' do
it 'does not delete trigger' do
- delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+ delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
expect(response).to have_http_status(403)
end
@@ -211,7 +309,7 @@ describe API::Triggers do
context 'unauthenticated user' do
it 'does not delete trigger' do
- delete api("/projects/#{project.id}/triggers/#{trigger.token}")
+ delete api("/projects/#{project.id}/triggers/#{trigger.id}")
expect(response).to have_http_status(401)
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 603da9f49fc..04e7837fd7a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -10,6 +10,8 @@ describe API::Users, api: true do
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+ let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 }
+ let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
describe "GET /users" do
context "when unauthenticated" do
@@ -540,10 +542,12 @@ describe API::Users, api: true do
it 'deletes existing key' do
user.keys << key
user.save
+
expect do
delete api("/users/#{user.id}/keys/#{key.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { user.keys.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 error if user not found' do
@@ -637,10 +641,12 @@ describe API::Users, api: true do
it 'deletes existing email' do
user.emails << email
user.save
+
expect do
delete api("/users/#{user.id}/emails/#{email.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { user.emails.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 error if user not found' do
@@ -671,10 +677,10 @@ describe API::Users, api: true do
it "deletes user" do
delete api("/users/#{user.id}", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
- expect(json_response['email']).to eq(user.email)
end
it "does not delete for unauthenticated user" do
@@ -724,7 +730,7 @@ describe API::Users, api: true do
get api("/user", user)
expect(response).to have_http_status(200)
- expect(response).to match_response_schema('user/public')
+ expect(response).to match_response_schema('public_api/v4/user/public')
expect(json_response['id']).to eq(user.id)
end
end
@@ -743,7 +749,7 @@ describe API::Users, api: true do
get api("/user?private_token=#{admin_personal_access_token}")
expect(response).to have_http_status(200)
- expect(response).to match_response_schema('user/public')
+ expect(response).to match_response_schema('public_api/v4/user/public')
expect(json_response['id']).to eq(admin.id)
end
end
@@ -753,7 +759,7 @@ describe API::Users, api: true do
get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200)
- expect(response).to match_response_schema('user/login')
+ expect(response).to match_response_schema('public_api/v4/user/login')
expect(json_response['id']).to eq(user.id)
end
@@ -761,7 +767,7 @@ describe API::Users, api: true do
get api("/user?private_token=#{admin.private_token}")
expect(response).to have_http_status(200)
- expect(response).to match_response_schema('user/public')
+ expect(response).to match_response_schema('public_api/v4/user/public')
expect(json_response['id']).to eq(admin.id)
end
end
@@ -869,10 +875,12 @@ describe API::Users, api: true do
it "deletes existed key" do
user.keys << key
user.save
+
expect do
delete api("/user/keys/#{key.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{user.keys.count}.by(-1)
- expect(response).to have_http_status(200)
end
it "returns 404 if key ID not found" do
@@ -976,10 +984,12 @@ describe API::Users, api: true do
it "deletes existed email" do
user.emails << email
user.save
+
expect do
delete api("/user/emails/#{email.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{user.emails.count}.by(-1)
- expect(response).to have_http_status(200)
end
it "returns 404 if email ID not found" do
@@ -1147,4 +1157,187 @@ describe API::Users, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
end
+
+ describe 'GET /users/:user_id/impersonation_tokens' do
+ let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
+ let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let!(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) }
+
+ it 'returns a 404 error if user not found' do
+ get api("/users/#{not_existing_user_id}/impersonation_tokens", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns a 403 error when authenticated as normal user' do
+ get api("/users/#{not_existing_user_id}/impersonation_tokens", user)
+
+ expect(response).to have_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'returns an array of all impersonated tokens' do
+ get api("/users/#{user.id}/impersonation_tokens", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ end
+
+ it 'returns an array of active impersonation tokens if state active' do
+ get api("/users/#{user.id}/impersonation_tokens?state=active", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response).to all(include('active' => true))
+ end
+
+ it 'returns an array of inactive personal access tokens if active is set to false' do
+ get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response).to all(include('active' => false))
+ end
+ end
+
+ describe 'POST /users/:user_id/impersonation_tokens' do
+ let(:name) { 'my new pat' }
+ let(:expires_at) { '2016-12-28' }
+ let(:scopes) { %w(api read_user) }
+ let(:impersonation) { true }
+
+ it 'returns validation error if impersonation token misses some attributes' do
+ post api("/users/#{user.id}/impersonation_tokens", admin)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'returns a 404 error if user not found' do
+ post api("/users/#{not_existing_user_id}/impersonation_tokens", admin),
+ name: name,
+ expires_at: expires_at
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns a 403 error when authenticated as normal user' do
+ post api("/users/#{user.id}/impersonation_tokens", user),
+ name: name,
+ expires_at: expires_at
+
+ expect(response).to have_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'creates a impersonation token' do
+ post api("/users/#{user.id}/impersonation_tokens", admin),
+ name: name,
+ expires_at: expires_at,
+ scopes: scopes,
+ impersonation: impersonation
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq(name)
+ expect(json_response['scopes']).to eq(scopes)
+ expect(json_response['expires_at']).to eq(expires_at)
+ expect(json_response['id']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['active']).to be_falsey
+ expect(json_response['revoked']).to be_falsey
+ expect(json_response['token']).to be_present
+ expect(json_response['impersonation']).to eq(impersonation)
+ end
+ end
+
+ describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ it 'returns 404 error if user not found' do
+ get api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns a 404 error if impersonation token not found' do
+ get api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+ end
+
+ it 'returns a 404 error if token is not impersonation token' do
+ get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+ end
+
+ it 'returns a 403 error when authenticated as normal user' do
+ get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+
+ expect(response).to have_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'returns a personal access token' do
+ get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['token']).to be_present
+ expect(json_response['impersonation']).to be_truthy
+ end
+ end
+
+ describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ it 'returns a 404 error if user not found' do
+ delete api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns a 404 error if impersonation token not found' do
+ delete api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+ end
+
+ it 'returns a 404 error if token is not impersonation token' do
+ delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Impersonation Token Not Found')
+ end
+
+ it 'returns a 403 error when authenticated as normal user' do
+ delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+
+ expect(response).to have_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'revokes a impersonation token' do
+ delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+
+ expect(response).to have_http_status(204)
+ expect(impersonation_token.revoked).to be_falsey
+ expect(impersonation_token.reload.revoked).to be_truthy
+ end
+ end
end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
new file mode 100644
index 00000000000..eeb4d128c1b
--- /dev/null
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -0,0 +1,299 @@
+require 'spec_helper'
+
+describe API::V3::AwardEmoji, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ let!(:note) { create(:note, project: project, noteable: issue) }
+
+ before { project.team << [user, :master] }
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
+ context 'on an issue' do
+ it "returns an array of award_emoji" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award_emoji.name)
+ end
+
+ it "returns a 404 error when issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it "returns an array of award_emoji" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(downvote.name)
+ end
+ end
+
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award.name)
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an array of award emoji' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(rocket.name)
+ end
+ end
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
+ context 'on an issue' do
+ it "returns the award emoji" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award_emoji.name)
+ expect(json_response['awardable_id']).to eq(issue.id)
+ expect(json_response['awardable_type']).to eq("Issue")
+ end
+
+ it "returns a 404 error if the award is not found" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it 'returns the award emoji' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(downvote.name)
+ expect(json_response['awardable_id']).to eq(merge_request.id)
+ expect(json_response['awardable_type']).to eq("MergeRequest")
+ end
+ end
+
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award.name)
+ expect(json_response['awardable_id']).to eq(snippet.id)
+ expect(json_response['awardable_type']).to eq("Snippet")
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an award emoji' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).not_to be_an Array
+ expect(json_response['name']).to eq(rocket.name)
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+ let(:issue2) { create(:issue, project: project, author: user) }
+
+ context "on an issue" do
+ it "creates a new award emoji" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if the name is not given" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if the user is not authenticated" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns a 404 error if the user authored issue" do
+ post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "normalizes +1 as thumbsup award" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+
+ expect(issue.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
+ end
+
+ context 'on a snippet' do
+ it 'creates a new award emoji' do
+ snippet = create(:project_snippet, :public, project: project)
+
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+ let(:note2) { create(:note, project: project, noteable: issue, author: user) }
+
+ it 'creates a new award emoji' do
+ expect do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ end.to change { note.award_emoji.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+
+ it "it returns 404 error when user authored note" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "normalizes +1 as thumbsup award" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+
+ expect(note.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
+ context 'when the awardable is an Issue' do
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
+ end
+
+ it 'returns a 404 error when the award emoji can not be found' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the awardable is a Merge Request' do
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the awardable is a Snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { note.award_emoji.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index 8aaf3be4f87..eb95934f354 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -5,6 +5,7 @@ describe API::V3::Boards, api: true do
let(:user) { create(:user) }
let(:guest) { create(:user) }
+ let(:non_member) { create(:user) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:dev_label) do
@@ -76,4 +77,37 @@ describe API::V3::Boards, api: true do
expect(response).to have_http_status(404)
end
end
+
+ describe "DELETE /projects/:id/board/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete v3_api("#{base_url}/#{dev_list.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a user with guest role from deleting a list" do
+ delete v3_api("#{base_url}/#{dev_list.id}", guest)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete v3_api("#{base_url}/44444", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the list if an admin requests it" do
+ delete v3_api("#{base_url}/#{dev_list.id}", owner)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index 0e4c6bc3bc6..5dcd4f21f4e 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -5,8 +5,13 @@ describe API::V3::Branches, api: true do
include ApiHelpers
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ let!(:branch_name) { 'feature' }
+ let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+ let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do
@@ -20,4 +25,111 @@ describe API::V3::Branches, api: true do
expect(branch_names).to match_array(project.repository.branch_names)
end
end
+
+ describe "DELETE /projects/:id/repository/branches/:branch" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it "removes branch" do
+ delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq(branch_name)
+ end
+
+ it "removes a branch with dots in the branch name" do
+ delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq("with.1.2.3")
+ end
+
+ it 'returns 404 if branch not exists' do
+ delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it "removes protected branch" do
+ create(:protected_branch, project: project, name: branch_name)
+ delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('Protected branch cant be removed')
+ end
+
+ it "does not remove HEAD branch" do
+ delete v3_api("/projects/#{project.id}/repository/branches/master", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('Cannot remove HEAD branch')
+ end
+ end
+
+ describe "DELETE /projects/:id/repository/merged_branches" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it 'returns 200' do
+ delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 403 error if guest' do
+ delete v3_api("/projects/#{project.id}/repository/merged_branches", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "POST /projects/:id/repository/branches" do
+ it "creates a new branch" do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'feature1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response['name']).to eq('feature1')
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ end
+
+ it "denies for user without push access" do
+ post v3_api("/projects/#{project.id}/repository/branches", user2),
+ branch_name: branch_name,
+ ref: branch_sha
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns 400 if branch name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new design',
+ ref: branch_sha
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch name is invalid')
+ end
+
+ it 'returns 400 if branch already exists' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+ expect(response).to have_http_status(201)
+
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch already exists')
+ end
+
+ it 'returns 400 if ref name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design3',
+ ref: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
end
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
new file mode 100644
index 00000000000..06556401a29
--- /dev/null
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe API::V3::BroadcastMessages, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'DELETE /broadcast_messages/:id' do
+ let!(:message) { create(:broadcast_message) }
+
+ it 'returns a 401 for anonymous users' do
+ delete v3_api("/broadcast_messages/#{message.id}"),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ delete v3_api("/broadcast_messages/#{message.id}", user),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'deletes the broadcast message for admins' do
+ expect do
+ delete v3_api("/broadcast_messages/#{message.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change { BroadcastMessage.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
new file mode 100644
index 00000000000..a50c22a6dd1
--- /dev/null
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -0,0 +1,489 @@
+require 'spec_helper'
+
+describe API::V3::Builds, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:api_user) { user }
+ let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
+ let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe 'GET /projects/:id/builds ' do
+ let(:query) { '' }
+
+ before do
+ create(:ci_build, :skipped, pipeline: pipeline)
+
+ get v3_api("/projects/#{project.id}/builds?#{query}", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns project builds' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
+ context 'filter project with one scope element' do
+ let(:query) { 'scope=pending' }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter project with scope skipped' do
+ let(:query) { 'scope=skipped' }
+ let(:json_build) { json_response.first }
+
+ it 'return builds with status skipped' do
+ expect(response).to have_http_status 200
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq 1
+ expect(json_build['status']).to eq 'skipped'
+ end
+ end
+
+ context 'filter project with array of scope elements' do
+ let(:query) { 'scope[0]=pending&scope[1]=running' }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { 'scope[0]=pending&scope[1]=unknown_status' }
+
+ it { expect(response).to have_http_status(400) }
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return project builds' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/builds' do
+ context 'when commit does not exist in repository' do
+ before do
+ get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
+ end
+
+ it 'responds with 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when commit exists in repository' do
+ context 'when user is authorized' do
+ context 'when pipeline has jobs' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+ create(:ci_build)
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
+ end
+
+ it 'returns project jobs for specific commit' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq 2
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+ end
+
+ context 'when pipeline has no jobs' do
+ before do
+ branch_head = project.commit('feature').id
+ get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
+ end
+
+ it 'returns an empty array' do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when user is not authorized' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
+ end
+
+ it 'does not return project jobs' do
+ expect(response).to have_http_status(401)
+ expect(json_response.except('message')).to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id' do
+ before do
+ get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job data' do
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('test')
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job data' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/artifacts' do
+ before do
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+ end
+
+ context 'job with artifacts' do
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ context 'authorized user' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'returns specific job artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ it 'does not return job artifacts if not uploaded' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:api_user) { reporter.user }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def path_for_ref(ref = pipeline.ref, job = build.name)
+ v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
+ end
+
+ context 'when not logged in' do
+ let(:api_user) { nil }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when logging as guest' do
+ let(:api_user) { guest.user }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'non-existing job' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get path_for_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such job' do
+ before do
+ get path_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper job' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get path_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get path_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/trace' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ before do
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job trace' do
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq(build.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job trace' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/cancel' do
+ before do
+ post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build persmission' do
+ it 'cancels running or pending job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/retry' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+ before do
+ post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build permission' do
+ it 'retries non-running job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/erase' do
+ before do
+ post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user)
+ end
+
+ context 'job is erasable' do
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+
+ it 'erases job content' do
+ expect(response.status).to eq 201
+ expect(build.trace).to be_empty
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'updates job' do
+ expect(build.reload.erased_at).to be_truthy
+ expect(build.reload.erased_by).to eq user
+ end
+ end
+
+ context 'job is not erasable' do
+ let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+
+ it 'responds with forbidden' do
+ expect(response.status).to eq 403
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
+ before do
+ post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response.status).to eq 200
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/play' do
+ before do
+ post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user)
+ end
+
+ context 'on an playable job' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the job' do
+ expect(response).to have_http_status 200
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable job' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Job")
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 2d7584c3e59..adba3a787aa 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do
end
end
- describe "Create a commit with multiple files and actions" do
+ describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project.id}/repository/commits" }
it 'returns a 403 unauthorized for user without permissions' do
@@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do
expect(response).to have_http_status(400)
end
- context :create do
+ describe 'create' do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
@@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do
expect(response).to have_http_status(400)
end
- context 'with project path in URL' do
- let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+ context 'with project path containing a dot in URL' do
+ let!(:user) { create(:user, username: 'foo.bar') }
+ let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
it 'a new file in project repo' do
post v3_api(url, user), valid_c_params
@@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do
end
end
- context :delete do
+ describe 'delete' do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
@@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do
end
end
- context :move do
+ describe 'move' do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
@@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do
end
end
- context :update do
+ describe 'update' do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
new file mode 100644
index 00000000000..3c5ce407b32
--- /dev/null
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe API::Deployments, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { deployment.environment.project }
+ let!(:deployment) { create(:deployment) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ describe 'GET /projects/:id/deployments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ end
+
+ it 'returns projects deployments' do
+ get api("/projects/#{project.id}/deployments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['iid']).to eq(deployment.iid)
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deployments/:deployment_id' do
+ context 'as a member of the project' do
+ it 'returns the projects deployment' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ expect(json_response['id']).to eq(deployment.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
new file mode 100644
index 00000000000..216192c9d34
--- /dev/null
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -0,0 +1,165 @@
+require 'spec_helper'
+
+describe API::V3::Environments, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private, namespace: user.namespace) }
+ let!(:environment) { create(:environment, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response.headers).to include('X-Total')
+ expect(response.headers).to include('X-Total-Pages')
+ expect(response.headers).to include('X-Per-Page')
+ expect(response.headers).to include('X-Page')
+ expect(response.headers).to include('X-Next-Page')
+ expect(response.headers).to include('X-Prev-Page')
+ expect(response.headers).to include('Link')
+ end
+ end
+
+ describe 'GET /projects/:id/environments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get v3_api("/projects/#{project.id}/environments", user) }
+ end
+
+ it 'returns project environments' do
+ get v3_api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['name']).to eq(environment.name)
+ expect(json_response.first['external_url']).to eq(environment.external_url)
+ expect(json_response.first['project']['id']).to eq(project.id)
+ expect(json_response.first['project']['visibility_level']).to be_present
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get v3_api("/projects/#{project.id}/environments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/environments' do
+ context 'as a member' do
+ it 'creates a environment with valid params' do
+ post v3_api("/projects/#{project.id}/environments", user), name: "mepmep"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('mepmep')
+ expect(json_response['slug']).to eq('mepmep')
+ expect(json_response['external']).to be nil
+ end
+
+ it 'requires name to be passed' do
+ post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 if environment already exists' do
+ post v3_api("/projects/#{project.id}/environments", user), name: environment.name
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 if slug is specified' do
+ post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 400 when the required params are missing' do
+ post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/environments/:environment_id' do
+ it 'returns a 200 if name and external_url are changed' do
+ url = 'https://mepmep.whatever.ninja'
+ put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
+ name: 'Mepmep', external_url: url
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ end
+
+ it "won't allow slug to be changed" do
+ slug = environment.slug
+ api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
+ put api_url, slug: slug + "-foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
+
+ it "won't update the external_url if only the name is passed" do
+ url = environment.external_url
+ put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
+ name: 'Mepmep'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ end
+
+ it 'returns a 404 if the environment does not exist' do
+ put v3_api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /projects/:id/environments/:environment_id' do
+ context 'as a master' do
+ it 'returns a 200 for an existing environment' do
+ delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 for non existing id' do
+ delete v3_api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 4af05605ec6..3b61139a2cd 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -2,17 +2,6 @@ require 'spec_helper'
describe API::V3::Files, api: true do
include ApiHelpers
- let(:user) { create(:user) }
- let!(:project) { create(:project, :repository, namespace: user.namespace ) }
- let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { 'files/ruby/popen.rb' }
- let(:params) do
- {
- file_path: file_path,
- ref: 'master'
- }
- end
- let(:author_email) { FFaker::Internet.email }
# I have to remove periods from the end of the name
# This happened when the user's name had a suffix (i.e. "Sr.")
@@ -26,6 +15,18 @@ describe API::V3::Files, api: true do
# ...
# Author: Foo Sr <foo@example.com>
# ...
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let(:guest) { create(:user) { |u| project.add_guest(u) } }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:params) do
+ {
+ file_path: file_path,
+ ref: 'master'
+ }
+ end
+ let(:author_email) { FFaker::Internet.email }
let(:author_name) { FFaker::Name.name.chomp("\.") }
before { project.team << [user, :developer] }
@@ -127,7 +128,7 @@ describe API::V3::Files, api: true do
end
it "returns a 400 if editor fails to create file" do
- allow_any_instance_of(Repository).to receive(:commit_file).
+ allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
@@ -147,6 +148,20 @@ describe API::V3::Files, api: true do
expect(last_commit.author_name).to eq(author_name)
end
end
+
+ context 'when the repo is empty' do
+ let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+
+ it "creates a new file in project repo" do
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['file_path']).to eq('newfile.rb')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ end
+ end
end
describe "PUT /projects/:id/repository/files" do
@@ -215,7 +230,7 @@ describe API::V3::Files, api: true do
end
it "returns a 400 if fails to create file" do
- allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
+ allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
new file mode 100644
index 00000000000..a71b7d4b008
--- /dev/null
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -0,0 +1,565 @@
+require 'spec_helper'
+
+describe API::V3::Groups, api: true do
+ include ApiHelpers
+ include UploadHelpers
+
+ let(:user1) { create(:user, can_create_group: false) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let!(:group2) { create(:group, :private) }
+ let!(:project1) { create(:empty_project, namespace: group1) }
+ let!(:project2) { create(:empty_project, namespace: group2) }
+ let!(:project3) { create(:empty_project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+
+ before do
+ group1.add_owner(user1)
+ group2.add_owner(user2)
+ end
+
+ describe "GET /groups" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/groups")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated as user" do
+ it "normal user: returns an array of groups of user1" do
+ get v3_api("/groups", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response)
+ .to satisfy_one { |group| group['name'] == group1.name }
+ end
+
+ it "does not include statistics" do
+ get v3_api("/groups", user1), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "admin: returns an array of all groups" do
+ get v3_api("/groups", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it "does not include statistics by default" do
+ get v3_api("/groups", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ attributes = {
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }.stringify_keys
+
+ project1.statistics.update!(attributes)
+
+ get v3_api("/groups", admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response)
+ .to satisfy_one { |group| group['statistics'] == attributes }
+ end
+ end
+
+ context "when using skip_groups in request" do
+ it "returns all groups excluding skipped groups" do
+ get v3_api("/groups", admin), skip_groups: [group2.id]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context "when using all_available in request" do
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
+ it "returns all groups you have access to" do
+ public_group = create :group, :public
+
+ get v3_api("/groups", user1), all_available: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to contain_exactly(public_group.name, group1.name)
+ end
+ end
+
+ context "when using sorting" do
+ let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
+ before do
+ group3.add_owner(user1)
+ end
+
+ it "sorts by name ascending by default" do
+ get v3_api("/groups", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group3.name, group1.name])
+ end
+
+ it "sorts in descending order when passed" do
+ get v3_api("/groups", user1), sort: "desc"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+
+ it "sorts by the order_by param" do
+ get v3_api("/groups", user1), order_by: "path"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+ end
+ end
+
+ describe 'GET /groups/owned' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/groups/owned')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as group owner' do
+ it 'returns an array of groups the user owns' do
+ get v3_api('/groups/owned', user2)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(group2.name)
+ end
+ end
+ end
+
+ describe "GET /groups/:id" do
+ context "when authenticated as user" do
+ it "returns one of user1's groups" do
+ project = create(:empty_project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ get v3_api("/groups/#{group1.id}", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(group1.id)
+ expect(json_response['name']).to eq(group1.name)
+ expect(json_response['path']).to eq(group1.path)
+ expect(json_response['description']).to eq(group1.description)
+ expect(json_response['visibility_level']).to eq(group1.visibility_level)
+ expect(json_response['avatar_url']).to eq(group1.avatar_url)
+ expect(json_response['web_url']).to eq(group1.web_url)
+ expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
+ expect(json_response['full_name']).to eq(group1.full_name)
+ expect(json_response['full_path']).to eq(group1.full_path)
+ expect(json_response['parent_id']).to eq(group1.parent_id)
+ expect(json_response['projects']).to be_an Array
+ expect(json_response['projects'].length).to eq(2)
+ expect(json_response['shared_projects']).to be_an Array
+ expect(json_response['shared_projects'].length).to eq(1)
+ expect(json_response['shared_projects'][0]['id']).to eq(project.id)
+ end
+
+ it "does not return a non existing group" do
+ get v3_api("/groups/1328", user1)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "does not return a group not attached to user1" do
+ get v3_api("/groups/#{group2.id}", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns any existing group" do
+ get v3_api("/groups/#{group2.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(group2.name)
+ end
+
+ it "does not return a non existing group" do
+ get v3_api("/groups/1328", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when using group path in URL' do
+ it 'returns any existing group' do
+ get v3_api("/groups/#{group1.path}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(group1.name)
+ end
+
+ it 'does not return a non existing group' do
+ get v3_api('/groups/unknown', admin)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not return a group not attached to user1' do
+ get v3_api("/groups/#{group2.path}", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'PUT /groups/:id' do
+ let(:new_group_name) { 'New Group'}
+
+ context 'when authenticated as the group owner' do
+ it 'updates the group' do
+ put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(new_group_name)
+ expect(json_response['request_access_enabled']).to eq(true)
+ end
+
+ it 'returns 404 for a non existing group' do
+ put v3_api('/groups/1328', user1), name: new_group_name
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated as the admin' do
+ it 'updates the group' do
+ put v3_api("/groups/#{group1.id}", admin), name: new_group_name
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(new_group_name)
+ end
+ end
+
+ context 'when authenticated as an user that can see the group' do
+ it 'does not updates the group' do
+ put v3_api("/groups/#{group1.id}", user2), name: new_group_name
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as an user that cannot see the group' do
+ it 'returns 404 when trying to update the group' do
+ put v3_api("/groups/#{group2.id}", user1), name: new_group_name
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "GET /groups/:id/projects" do
+ context "when authenticated as user" do
+ it "returns the group's projects" do
+ get v3_api("/groups/#{group1.id}/projects", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name'] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['visibility_level']).to be_present
+ end
+
+ it "returns the group's projects with simple representation" do
+ get v3_api("/groups/#{group1.id}/projects", user1), simple: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name'] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['visibility_level']).not_to be_present
+ end
+
+ it 'filters the groups projects' do
+ public_project = create(:empty_project, :public, path: 'test1', group: group1)
+
+ get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(public_project.name)
+ end
+
+ it "does not return a non existing group" do
+ get v3_api("/groups/1328/projects", user1)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "does not return a group not attached to user1" do
+ get v3_api("/groups/#{group2.id}/projects", user1)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "only returns projects to which user has access" do
+ project3.team << [user3, :developer]
+
+ get v3_api("/groups/#{group1.id}/projects", user3)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project3.name)
+ end
+
+ it 'only returns the projects owned by user' do
+ project2.group.add_owner(user3)
+
+ get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project2.name)
+ end
+
+ it 'only returns the projects starred by user' do
+ user1.starred_projects = [project1]
+
+ get v3_api("/groups/#{group1.id}/projects", user1), starred: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project1.name)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns any existing group" do
+ get v3_api("/groups/#{group2.id}/projects", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project2.name)
+ end
+
+ it "does not return a non existing group" do
+ get v3_api("/groups/1328/projects", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when using group path in URL' do
+ it 'returns any existing group' do
+ get v3_api("/groups/#{group1.path}/projects", admin)
+
+ expect(response).to have_http_status(200)
+ project_names = json_response.map { |proj| proj['name'] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ end
+
+ it 'does not return a non existing group' do
+ get v3_api('/groups/unknown/projects', admin)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not return a group not attached to user1' do
+ get v3_api("/groups/#{group2.path}/projects", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "POST /groups" do
+ context "when authenticated as user without group permissions" do
+ it "does not create group" do
+ post v3_api("/groups", user1), attributes_for(:group)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when authenticated as user with group permissions" do
+ it "creates group" do
+ group = attributes_for(:group, { request_access_enabled: false })
+
+ post v3_api("/groups", user3), group
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response["name"]).to eq(group[:name])
+ expect(json_response["path"]).to eq(group[:path])
+ expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
+ end
+
+ it "creates a nested group" do
+ parent = create(:group)
+ parent.add_owner(user3)
+ group = attributes_for(:group, { parent_id: parent.id })
+
+ post v3_api("/groups", user3), group
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}")
+ expect(json_response["parent_id"]).to eq(parent.id)
+ end
+
+ it "does not create group, duplicate" do
+ post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
+
+ expect(response).to have_http_status(400)
+ expect(response.message).to eq("Bad Request")
+ end
+
+ it "returns 400 bad request error if name not given" do
+ post v3_api("/groups", user3), { path: group2.path }
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 bad request error if path not given" do
+ post v3_api("/groups", user3), { name: 'test' }
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "DELETE /groups/:id" do
+ context "when authenticated as user" do
+ it "removes group" do
+ delete v3_api("/groups/#{group1.id}", user1)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "does not remove a group if not an owner" do
+ user4 = create(:user)
+ group1.add_master(user4)
+
+ delete v3_api("/groups/#{group1.id}", user3)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "does not remove a non existing group" do
+ delete v3_api("/groups/1328", user1)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "does not remove a group not attached to user1" do
+ delete v3_api("/groups/#{group2.id}", user1)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "removes any existing group" do
+ delete v3_api("/groups/#{group2.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "does not remove a non existing group" do
+ delete v3_api("/groups/1328", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "POST /groups/:id/projects/:project_id" do
+ let(:project) { create(:empty_project) }
+ let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+
+ before(:each) do
+ allow_any_instance_of(Projects::TransferService).
+ to receive(:execute).and_return(true)
+ end
+
+ context "when authenticated as user" do
+ it "does not transfer project to group" do
+ post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "transfers project to group" do
+ post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when using project path in URL' do
+ context 'with a valid project path' do
+ it "transfers project to group" do
+ post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'with a non-existent project path' do
+ it "does not transfer project to group" do
+ post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when using a group path in URL' do
+ context 'with a valid group path' do
+ it "transfers project to group" do
+ post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'with a non-existent group path' do
+ it "does not transfer project to group" do
+ post v3_api("/groups/noexist/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 8e6732fe23e..51021eec63c 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -232,6 +232,13 @@ describe API::V3::Issues, api: true do
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
+
+ it 'matches V3 response schema' do
+ get v3_api('/issues', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v3/issues')
+ end
end
end
@@ -432,6 +439,12 @@ describe API::V3::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ it 'returns 404 when project does not exist' do
+ get v3_api('/projects/1000/issues', non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
@@ -722,7 +735,7 @@ describe API::V3::Issues, api: true do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
end
@@ -1281,6 +1294,6 @@ describe API::V3::Issues, api: true do
describe 'time tracking endpoints' do
let(:issuable) { issue }
- include_examples 'time tracking endpoints', 'issue'
+ include_examples 'V3 time tracking endpoints', 'issue'
end
end
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index bcb0c6b9449..dfac357d37c 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -21,11 +21,11 @@ describe API::V3::Labels, api: true do
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
- expected_keys = [
- 'id', 'name', 'color', 'description',
- 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
- 'subscribed', 'priority'
- ]
+ expected_keys = %w(
+ id name color description
+ open_issues_count closed_issues_count open_merge_requests_count
+ subscribed priority
+ )
get v3_api("/projects/#{project.id}/labels", user)
@@ -149,4 +149,23 @@ describe API::V3::Labels, api: true do
end
end
end
+
+ describe 'DELETE /projects/:id/labels' do
+ it 'returns 200 for existing label' do
+ delete v3_api("/projects/#{project.id}/labels", user), name: 'label1'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 for non existing label' do
+ delete v3_api("/projects/#{project.id}/labels", user), name: 'label2'
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Label Not Found')
+ end
+
+ it 'returns 400 for wrong parameters' do
+ delete v3_api("/projects/#{project.id}/labels", user)
+ expect(response).to have_http_status(400)
+ end
+ end
end
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index 28c3ca03960..13814ed10c3 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Members, api: true do
+describe API::V3::Members, api: true do
include ApiHelpers
let(:master) { create(:user) }
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
new file mode 100644
index 00000000000..c53800eef30
--- /dev/null
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -0,0 +1,50 @@
+require "spec_helper"
+
+describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
+ include ApiHelpers
+
+ let!(:user) { create(:user) }
+ let!(:merge_request) { create(:merge_request, importing: true) }
+ let!(:project) { merge_request.target_project }
+
+ before do
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ it 'returns 200 for a valid merge request' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+ merge_request_diff = merge_request.merge_request_diffs.first
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
+ expect(json_response.first['id']).to eq(merge_request_diff.id)
+ expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/versions", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ it 'returns a 200 for a valid merge request' do
+ merge_request_diff = merge_request.merge_request_diffs.first
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response['id']).to eq(merge_request_diff.id)
+ expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+ expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index b94e1ef4ced..d73e9635c9b 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -73,6 +73,13 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
+ it 'matches V3 response schema' do
+ get v3_api("/projects/#{project.id}/merge_requests", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v3/merge_requests')
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -237,7 +244,7 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['force_remove_source_branch']).to be_truthy
end
@@ -705,7 +712,7 @@ describe API::MergeRequests, api: true do
describe 'Time tracking' do
let(:issuable) { merge_request }
- include_examples 'time tracking endpoints', 'merge_request'
+ include_examples 'V3 time tracking endpoints', 'merge_request'
end
def mr_with_later_created_and_updated_at_time
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
new file mode 100644
index 00000000000..127c0eec881
--- /dev/null
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -0,0 +1,239 @@
+require 'spec_helper'
+
+describe API::V3::Milestones, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let!(:project) { create(:empty_project, namespace: user.namespace ) }
+ let!(:closed_milestone) { create(:closed_milestone, project: project) }
+ let!(:milestone) { create(:milestone, project: project) }
+
+ before { project.team << [user, :developer] }
+
+ describe 'GET /projects/:id/milestones' do
+ it 'returns project milestones' do
+ get v3_api("/projects/#{project.id}/milestones", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(milestone.title)
+ end
+
+ it 'returns a 401 error if user not authenticated' do
+ get v3_api("/projects/#{project.id}/milestones")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns an array of active milestones' do
+ get v3_api("/projects/#{project.id}/milestones?state=active", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(milestone.id)
+ end
+
+ it 'returns an array of closed milestones' do
+ get v3_api("/projects/#{project.id}/milestones?state=closed", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_milestone.id)
+ end
+ end
+
+ describe 'GET /projects/:id/milestones/:milestone_id' do
+ it 'returns a project milestone by id' do
+ get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(milestone.title)
+ expect(json_response['iid']).to eq(milestone.iid)
+ end
+
+ it 'returns a project milestone by iid' do
+ get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['title']).to eq closed_milestone.title
+ expect(json_response.first['id']).to eq closed_milestone.id
+ end
+
+ it 'returns a project milestone by iid array' do
+ get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['title']).to eq milestone.title
+ expect(json_response.first['id']).to eq milestone.id
+ end
+
+ it 'returns 401 error if user not authenticated' do
+ get v3_api("/projects/#{project.id}/milestones/#{milestone.id}")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 error if milestone id not found' do
+ get v3_api("/projects/#{project.id}/milestones/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/milestones' do
+ it 'creates a new project milestone' do
+ post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new milestone')
+ expect(json_response['description']).to be_nil
+ end
+
+ it 'creates a new project milestone with description and dates' do
+ post v3_api("/projects/#{project.id}/milestones", user),
+ title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['description']).to eq('release')
+ expect(json_response['due_date']).to eq('2013-03-02')
+ expect(json_response['start_date']).to eq('2013-02-02')
+ end
+
+ it 'returns a 400 error if title is missing' do
+ post v3_api("/projects/#{project.id}/milestones", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 error if params are invalid (duplicate title)' do
+ post v3_api("/projects/#{project.id}/milestones", user),
+ title: milestone.title, description: 'release', due_date: '2013-03-02'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a new project with reserved html characters' do
+ post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
+ expect(json_response['description']).to be_nil
+ end
+ end
+
+ describe 'PUT /projects/:id/milestones/:milestone_id' do
+ it 'updates a project milestone' do
+ put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'removes a due date if nil is passed' do
+ milestone.update!(due_date: "2016-08-05")
+
+ put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to be_nil
+ end
+
+ it 'returns a 404 error if milestone id not found' do
+ put v3_api("/projects/#{project.id}/milestones/1234", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
+ it 'updates a project milestone' do
+ put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ state_event: 'close'
+ expect(response).to have_http_status(200)
+
+ expect(json_response['state']).to eq('closed')
+ end
+ end
+
+ describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
+ it 'creates an activity event when an milestone is closed' do
+ expect(Event).to receive(:create)
+
+ put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ state_event: 'close'
+ end
+ end
+
+ describe 'GET /projects/:id/milestones/:milestone_id/issues' do
+ before do
+ milestone.issues << create(:issue, project: project)
+ end
+ it 'returns project issues for a particular milestone' do
+ get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['milestone']['title']).to eq(milestone.title)
+ end
+
+ it 'matches V3 response schema for a list of issues' do
+ get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v3/issues')
+ end
+
+ it 'returns a 401 error if user not authenticated' do
+ get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+
+ expect(response).to have_http_status(401)
+ end
+
+ describe 'confidential issues' do
+ let(:public_project) { create(:empty_project, :public) }
+ let(:milestone) { create(:milestone, project: public_project) }
+ let(:issue) { create(:issue, project: public_project) }
+ let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
+ before do
+ public_project.team << [user, :developer]
+ milestone.issues << issue << confidential_issue
+ end
+
+ it 'returns confidential issues to team members' do
+ get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
+ end
+
+ it 'does not return confidential issues to team members with guest role' do
+ member = create(:user)
+ project.team << [member, :guest]
+
+ get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+
+ it 'does not return confidential issues to regular users' do
+ get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
new file mode 100644
index 00000000000..ddef2d5eb04
--- /dev/null
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -0,0 +1,433 @@
+require 'spec_helper'
+
+describe API::V3::Notes, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
+ let!(:issue) { create(:issue, project: project, author: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
+ let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
+ let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
+
+ # For testing the cross-reference of a private issue in a public issue
+ let(:private_user) { create(:user) }
+ let(:private_project) do
+ create(:empty_project, namespace: private_user.namespace).
+ tap { |p| p.team << [private_user, :master] }
+ end
+ let(:private_issue) { create(:issue, project: private_project) }
+
+ let(:ext_proj) { create(:empty_project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let!(:cross_reference_note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
+ before { project.team << [user, :reporter] }
+
+ describe "GET /projects/:id/noteable/:noteable_id/notes" do
+ context "when noteable is an Issue" do
+ it "returns an array of issue notes" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(issue_note.note)
+ expect(json_response.first['upvote']).to be_falsey
+ expect(json_response.first['downvote']).to be_falsey
+ end
+
+ it "returns a 404 error when issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/12345/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "and current user cannot view the notes" do
+ it "returns an empty array" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+
+ context "and issue is confidential" do
+ before { ext_issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "and current user can view the note" do
+ it "returns an empty array" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "returns an array of snippet notes" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(snippet_note.note)
+ end
+
+ it "returns a 404 error when snippet id not found" do
+ get v3_api("/projects/#{project.id}/snippets/42/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when noteable is a Merge Request" do
+ it "returns an array of merge_requests notes" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(merge_request_note.note)
+ end
+
+ it "returns a 404 error if merge request id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
+ context "when noteable is an Issue" do
+ it "returns an issue note by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(issue_note.note)
+ end
+
+ it "returns a 404 error if issue note not found" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "and current user cannot view the note" do
+ it "returns a 404 error" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when issue is confidential" do
+ before { issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "and current user can view the note" do
+ it "returns an issue note by id" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "returns a snippet note by id" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(snippet_note.note)
+ end
+
+ it "returns a 404 error if snippet note not found" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/noteable/:noteable_id/notes" do
+ context "when noteable is an Issue" do
+ it "creates a new issue note" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
+
+ expect(response).to have_http_status(401)
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ body: 'hi!', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'when the user is posting an award emoji on an issue created by someone else' do
+ let(:issue2) { create(:issue, project: project) }
+
+ it 'creates a new issue note' do
+ post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq(':+1:')
+ end
+ end
+
+ context 'when the user is posting an award emoji on his/her own issue' do
+ it 'creates a new issue note' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq(':+1:')
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "creates a new snippet note" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when user does not have access to read the noteable' do
+ it 'responds with 404' do
+ project = create(:empty_project, :private) { |p| p.add_guest(user) }
+ issue = create(:issue, :confidential, project: project)
+
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ body: 'Foo'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user does not have access to create noteable' do
+ let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
+
+ ##
+ # We are posting to project user has access to, but we use issue id
+ # from a different project, see #15577
+ #
+ before do
+ post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user),
+ body: 'Hi!'
+ end
+
+ it 'responds with resource not found error' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not create new note' do
+ expect(private_issue.notes.reload).to be_empty
+ end
+ end
+ end
+
+ describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
+ it "creates an activity event when an issue note is created" do
+ expect(Event).to receive(:create)
+
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+ end
+ end
+
+ describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
+ context 'when noteable is an Issue' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if body not given' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'when noteable is a Snippet' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/12345", user), body: "Hello!"
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ "notes/#{merge_request_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ "notes/12345", user), body: "Hello!"
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
+ context 'when noteable is an Issue' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Snippet' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
new file mode 100644
index 00000000000..3786eb06932
--- /dev/null
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -0,0 +1,203 @@
+require 'spec_helper'
+
+describe API::V3::Pipelines, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines ' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) }
+ end
+
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get v3_api("/projects/#{project.id}/pipelines", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['sha']).to match(/\A\h{40}\z/)
+ expect(json_response.first['id']).to eq pipeline.id
+ expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage])
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get v3_api("/projects/#{project.id}/pipelines", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipeline ' do
+ context 'authorized user' do
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it 'creates and returns a new pipeline' do
+ expect do
+ post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+ end.to change { Ci::Pipeline.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to be_a Hash
+ expect(json_response['sha']).to eq project.commit.id
+ end
+
+ it 'fails when using an invalid ref' do
+ post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Reference not found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+
+ context 'without gitlab-ci.yml' do
+ it 'fails to create pipeline' do
+ post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not create pipeline' do
+ post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id' do
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ end
+
+ it 'returns 404 when it does not exist' do
+ get v3_api("/projects/#{project.id}/pipelines/123456", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Not found'
+ expect(json_response['id']).to be nil
+ end
+
+ context 'with coverage' do
+ before do
+ create(:ci_build, coverage: 30, pipeline: pipeline)
+ end
+
+ it 'exposes the coverage' do
+ get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(json_response["coverage"].to_i).to eq(30)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
+ context 'authorized user' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ it 'retries failed builds' do
+ expect do
+ post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+ end.to change { pipeline.builds.count }.from(1).to(2)
+
+ expect(response).to have_http_status(201)
+ expect(build.reload.retried?).to be true
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ context 'authorized user' do
+ it 'retries failed builds' do
+ post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('canceled')
+ end
+ end
+
+ context 'user without proper access rights' do
+ let!(:reporter) { create(:user) }
+
+ before { project.team << [reporter, :reporter] }
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
+
+ expect(response).to have_http_status(403)
+ expect(pipeline.reload.status).to eq('pending')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
new file mode 100644
index 00000000000..a981119dc5a
--- /dev/null
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -0,0 +1,216 @@
+require 'spec_helper'
+
+describe API::ProjectHooks, 'ProjectHooks', api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user3) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:hook) do
+ create(:project_hook,
+ :all_events_enabled,
+ project: project,
+ url: 'http://example.com',
+ enable_ssl_verification: true)
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user3, :developer]
+ end
+
+ describe "GET /projects/:id/hooks" do
+ context "authorized user" do
+ it "returns project hooks" do
+ get v3_api("/projects/#{project.id}/hooks", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['url']).to eq("http://example.com")
+ expect(json_response.first['issues_events']).to eq(true)
+ expect(json_response.first['push_events']).to eq(true)
+ expect(json_response.first['merge_requests_events']).to eq(true)
+ expect(json_response.first['tag_push_events']).to eq(true)
+ expect(json_response.first['note_events']).to eq(true)
+ expect(json_response.first['build_events']).to eq(true)
+ expect(json_response.first['pipeline_events']).to eq(true)
+ expect(json_response.first['wiki_page_events']).to eq(true)
+ expect(json_response.first['enable_ssl_verification']).to eq(true)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not access project hooks" do
+ get v3_api("/projects/#{project.id}/hooks", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe "GET /projects/:id/hooks/:hook_id" do
+ context "authorized user" do
+ it "returns a project hook" do
+ get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['url']).to eq(hook.url)
+ expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['push_events']).to eq(hook.push_events)
+ expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+ expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+ expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+ expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+ end
+
+ it "returns a 404 error if hook id is not available" do
+ get v3_api("/projects/#{project.id}/hooks/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not access an existing hook" do
+ get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it "returns a 404 error if hook id is not available" do
+ get v3_api("/projects/#{project.id}/hooks/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/hooks" do
+ it "adds hook to project" do
+ expect do
+ post v3_api("/projects/#{project.id}/hooks", user),
+ url: "http://example.com", issues_events: true, wiki_page_events: true
+ end.to change {project.hooks.count}.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['url']).to eq('http://example.com')
+ expect(json_response['issues_events']).to eq(true)
+ expect(json_response['push_events']).to eq(true)
+ expect(json_response['merge_requests_events']).to eq(false)
+ expect(json_response['tag_push_events']).to eq(false)
+ expect(json_response['note_events']).to eq(false)
+ expect(json_response['build_events']).to eq(false)
+ expect(json_response['pipeline_events']).to eq(false)
+ expect(json_response['wiki_page_events']).to eq(true)
+ expect(json_response['enable_ssl_verification']).to eq(true)
+ expect(json_response).not_to include('token')
+ end
+
+ it "adds the token without including it in the response" do
+ token = "secret token"
+
+ expect do
+ post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
+ end.to change {project.hooks.count}.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["url"]).to eq("http://example.com")
+ expect(json_response).not_to include("token")
+
+ hook = project.hooks.find(json_response["id"])
+
+ expect(hook.url).to eq("http://example.com")
+ expect(hook.token).to eq(token)
+ end
+
+ it "returns a 400 error if url not given" do
+ post v3_api("/projects/#{project.id}/hooks", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 422 error if url not valid" do
+ post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ describe "PUT /projects/:id/hooks/:hook_id" do
+ it "updates an existing project hook" do
+ put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user),
+ url: 'http://example.org', push_events: false
+ expect(response).to have_http_status(200)
+ expect(json_response['url']).to eq('http://example.org')
+ expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['push_events']).to eq(false)
+ expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+ expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+ expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+ expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+ end
+
+ it "adds the token without including it in the response" do
+ token = "secret token"
+
+ put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
+
+ expect(response).to have_http_status(200)
+ expect(json_response["url"]).to eq("http://example.org")
+ expect(json_response).not_to include("token")
+
+ expect(hook.reload.url).to eq("http://example.org")
+ expect(hook.reload.token).to eq(token)
+ end
+
+ it "returns 404 error if hook id not found" do
+ put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 400 error if url is not given" do
+ put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 422 error if url is not valid" do
+ put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ describe "DELETE /projects/:id/hooks/:hook_id" do
+ it "deletes hook from project" do
+ expect do
+ delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+ end.to change {project.hooks.count}.by(-1)
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns success when deleting hook" do
+ delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns a 404 error when deleting non existent hook" do
+ delete v3_api("/projects/#{project.id}/hooks/42", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns a 404 error if hook id not given" do
+ delete v3_api("/projects/#{project.id}/hooks", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
+ test_user = create(:user)
+ other_project = create(:project)
+ other_project.team << [test_user, :master]
+
+ delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
+ expect(response).to have_http_status(404)
+ expect(WebHook.exists?(hook.id)).to be_truthy
+ end
+ end
+end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 36d99d80e79..d8bb562587d 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -84,7 +84,7 @@ describe API::V3::Projects, api: true do
context 'GET /projects?simple=true' do
it 'returns a simplified version of all the projects' do
- expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+ expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
get v3_api('/projects?simple=true', user)
@@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do
end
end
- it 'creates new project without path and return 201' do
- expect { post v3_api('/projects', user), name: 'foo' }.
+ it 'creates new project without path but with name and returns 201' do
+ expect { post v3_api('/projects', user), name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project without name but with path and returns 201' do
+ expect { post v3_api('/projects', user), path: 'foo_project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('foo_project')
+ expect(project.path).to eq('foo_project')
+ end
+
+ it 'creates new project name and path and returns 201' do
+ expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-Project')
end
it 'creates last project before reaching project limit' do
@@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do
expect(response).to have_http_status(201)
end
- it 'does not create new project without name and return 400' do
+ it 'does not create new project without name or path and return 400' do
expect { post v3_api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
@@ -400,7 +427,7 @@ describe API::V3::Projects, api: true do
expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
end
- it 'sets a project as allowing merge only if build succeeds' do
+ it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
post v3_api('/projects', user), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
@@ -545,7 +572,7 @@ describe API::V3::Projects, api: true do
expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
end
- it 'sets a project as allowing merge only if build succeeds' do
+ it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
post v3_api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
@@ -642,7 +669,7 @@ describe API::V3::Projects, api: true do
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
- expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index c696721c1c9..fef6fb641fa 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -3,6 +3,8 @@ require 'mime/types'
describe API::V3::Repositories, api: true do
include ApiHelpers
+ include RepoHelpers
+ include WorkhorseHelpers
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
@@ -96,6 +98,226 @@ describe API::V3::Repositories, api: true do
end
end
+ {
+ 'blobs/:sha' => 'blobs/master',
+ 'commits/:sha/blob' => 'commits/master/blob'
+ }.each do |desc_path, example_path|
+ describe "GET /projects/:id/repository/#{desc_path}" do
+ let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+ shared_examples_for 'repository blob' do
+ it 'returns the repository blob' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+ context 'when filepath does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+ context 'when no filepath is given' do
+ it_behaves_like '400 response' do
+ let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) }
+ end
+ end
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository blob' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+ end
+ describe "GET /projects/:id/repository/raw_blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+ shared_examples_for 'repository raw blob' do
+ it 'returns the repository raw blob' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) }
+ let(:message) { '404 Blob Not Found' }
+ end
+ end
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository raw blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository raw blob' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+ describe "GET /projects/:id/repository/archive(.:format)?:sha" do
+ let(:route) { "/projects/#{project.id}/repository/archive" }
+ shared_examples_for 'repository archive' do
+ it 'returns the repository archive' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ end
+ it 'returns the repository archive archive.zip' do
+ get v3_api("/projects/#{project.id}/repository/archive.zip", user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ end
+ it 'returns the repository archive archive.tar.bz2' do
+ get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?sha=xxx", current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository archive' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository archive' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/compare' do
+ let(:route) { "/projects/#{project.id}/repository/compare" }
+ shared_examples_for 'repository compare' do
+ it "compares branches" do
+ get v3_api(route, current_user), from: 'master', to: 'feature'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares tags" do
+ get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares commits" do
+ get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_falsey
+ end
+ it "compares commits in reverse order" do
+ get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares same refs" do
+ get v3_api(route, current_user), from: 'master', to: 'master'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_truthy
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository compare' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository compare' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+
describe 'GET /projects/:id/repository/contributors' do
let(:route) { "/projects/#{project.id}/repository/contributors" }
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
new file mode 100644
index 00000000000..ca335ce9cf0
--- /dev/null
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe API::V3::Runners, api: true do
+ include ApiHelpers
+
+ let(:admin) { create(:user, :admin) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
+
+ let!(:shared_runner) { create(:ci_runner, :shared) }
+ let!(:unused_specific_runner) { create(:ci_runner) }
+
+ let!(:specific_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ end
+ end
+
+ let!(:two_projects_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ create(:ci_runner_project, runner: runner, project: project2)
+ end
+ end
+
+ before do
+ # Set project access for users
+ create(:project_member, :master, user: user, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
+ end
+
+ describe 'DELETE /runners/:id' do
+ context 'admin user' do
+ context 'when runner is shared' do
+ it 'deletes runner' do
+ expect do
+ delete v3_api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.shared.count }.by(-1)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'deletes unused runner' do
+ expect do
+ delete v3_api("/runners/#{unused_specific_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+
+ it 'deletes used runner' do
+ expect do
+ delete v3_api("/runners/#{specific_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+ end
+
+ it 'returns 404 if runner does not exists' do
+ delete v3_api('/runners/9999', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authorized user' do
+ context 'when runner is shared' do
+ it 'does not delete runner' do
+ delete v3_api("/runners/#{shared_runner.id}", user)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'does not delete runner without access to it' do
+ delete v3_api("/runners/#{specific_runner.id}", user2)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not delete runner with more than one associated project' do
+ delete v3_api("/runners/#{two_projects_runner.id}", user)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'deletes runner for one owned project' do
+ expect do
+ delete v3_api("/runners/#{specific_runner.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not delete runner' do
+ delete v3_api("/runners/#{specific_runner.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/runners/:runner_id' do
+ context 'authorized user' do
+ context 'when runner have more than one associated projects' do
+ it "disables project's runner" do
+ expect do
+ delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{ project.runners.count }.by(-1)
+ end
+ end
+
+ context 'when runner have one associated projects' do
+ it "does not disable project's runner" do
+ expect do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
+ end.to change{ project.runners.count }.by(0)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'returns 404 is runner is not found' do
+ delete v3_api("/projects/#{project.id}/runners/9999", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authorized user without permissions' do
+ it "does not disable project's runner" do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it "does not disable project's runner" do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
new file mode 100644
index 00000000000..3a760a8f25c
--- /dev/null
+++ b/spec/requests/api/v3/services_spec.rb
@@ -0,0 +1,24 @@
+require "spec_helper"
+
+describe API::V3::Services, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+
+ available_services = Service.available_services_names
+ available_services.delete('prometheus')
+ available_services.each do |service|
+ describe "DELETE /projects/:id/services/#{service.dasherize}" do
+ include_context service
+
+ it "deletes #{service}" do
+ delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user)
+
+ expect(response).to have_http_status(200)
+ project.send(service_method).reload
+ expect(project.send(service_method).activated?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
new file mode 100644
index 00000000000..a9fa5adac17
--- /dev/null
+++ b/spec/requests/api/v3/settings_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe API::V3::Settings, 'Settings', api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe "GET /application/settings" do
+ it "returns application settings" do
+ get v3_api("/application/settings", admin)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Hash
+ expect(json_response['default_projects_limit']).to eq(42)
+ expect(json_response['signin_enabled']).to be_truthy
+ expect(json_response['repository_storage']).to eq('default')
+ expect(json_response['koding_enabled']).to be_falsey
+ expect(json_response['koding_url']).to be_nil
+ expect(json_response['plantuml_enabled']).to be_falsey
+ expect(json_response['plantuml_url']).to be_nil
+ end
+ end
+
+ describe "PUT /application/settings" do
+ context "custom repository storage type set in the config" do
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ it "updates application settings" do
+ put v3_api("/application/settings", admin),
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
+ plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
+ expect(response).to have_http_status(200)
+ expect(json_response['default_projects_limit']).to eq(3)
+ expect(json_response['signin_enabled']).to be_falsey
+ expect(json_response['repository_storage']).to eq('custom')
+ expect(json_response['repository_storages']).to eq(['custom'])
+ expect(json_response['koding_enabled']).to be_truthy
+ expect(json_response['koding_url']).to eq('http://koding.example.com')
+ expect(json_response['plantuml_enabled']).to be_truthy
+ expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+ end
+ end
+
+ context "missing koding_url value when koding_enabled is true" do
+ it "returns a blank parameter error message" do
+ put v3_api("/application/settings", admin), koding_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('koding_url is missing')
+ end
+ end
+
+ context "missing plantuml_url value when plantuml_enabled is true" do
+ it "returns a blank parameter error message" do
+ put v3_api("/application/settings", admin), plantuml_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('plantuml_url is missing')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
new file mode 100644
index 00000000000..05653bd0d51
--- /dev/null
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -0,0 +1,187 @@
+require 'rails_helper'
+
+describe API::V3::Snippets, api: true do
+ include ApiHelpers
+ let!(:user) { create(:user) }
+
+ describe 'GET /snippets/' do
+ it 'returns snippets available' do
+ public_snippet = create(:personal_snippet, :public, author: user)
+ private_snippet = create(:personal_snippet, :private, author: user)
+ internal_snippet = create(:personal_snippet, :internal, author: user)
+
+ get v3_api("/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ internal_snippet.id,
+ private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last).to have_key('raw_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:personal_snippet, :private)
+
+ get v3_api("/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'GET /snippets/public' do
+ let!(:other_user) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+ let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+ let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+ let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+ it 'returns all snippets with public visibility from all users' do
+ get v3_api("/snippets/public", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ public_snippet_other.id)
+ expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}",
+ "http://localhost/snippets/#{public_snippet_other.id}")
+ expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}/raw",
+ "http://localhost/snippets/#{public_snippet_other.id}/raw")
+ end
+ end
+
+ describe 'GET /snippets/:id/raw' do
+ let(:snippet) { create(:personal_snippet, author: user) }
+
+ it 'returns raw text' do
+ get v3_api("/snippets/#{snippet.id}/raw", user)
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to eq(snippet.content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'POST /snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ content: 'puts "hello world"',
+ visibility_level: Snippet::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ expect do
+ post v3_api("/snippets/", user), params
+ end.to change { PersonalSnippet.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(params[:title])
+ expect(json_response['file_name']).to eq(params[:file_name])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post v3_api("/snippets/", user), params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when the snippet is spam' do
+ def create_snippet(snippet_params = {})
+ post v3_api('/snippets', user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'PUT /snippets/:id' do
+ let(:other_user) { create(:user) }
+ let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put v3_api("/snippets/#{public_snippet.id}", user), content: new_content
+
+ expect(response).to have_http_status(200)
+ public_snippet.reload
+ expect(public_snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put v3_api("/snippets/1234", user), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it "returns 404 for another user's snippet" do
+ put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ put v3_api("/snippets/1234", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /snippets/:id' do
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'deletes snippet' do
+ expect do
+ delete v3_api("/snippets/#{public_snippet.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { PersonalSnippet.count }.by(-1)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index da58efb6ebf..91038977c82 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -38,4 +38,20 @@ describe API::V3::SystemHooks, api: true do
end
end
end
+
+ describe "DELETE /hooks/:id" do
+ it "deletes a hook" do
+ expect do
+ delete v3_api("/hooks/#{hook.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change { SystemHook.count }.by(-1)
+ end
+
+ it 'returns 404 if the system hook does not exist' do
+ delete v3_api('/hooks/12345', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
index 6722789d928..6870cfd2668 100644
--- a/spec/requests/api/v3/tags_spec.rb
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -64,4 +64,26 @@ describe API::V3::Tags, api: true do
end
end
end
+
+ describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+ end
+
+ context 'delete tag' do
+ it 'deletes an existing tag' do
+ delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ end
+
+ it 'raises 404 if the tag does not exist' do
+ delete v3_api("/projects/#{project.id}/repository/tags/foobar", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 4fd4e70bedd..f1e554b98cc 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -56,11 +56,11 @@ describe API::V3::Templates, api: true do
expect(json_response['popular']).to be true
expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
- expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
expect(json_response['limitations']).to eq(%w[no-liability])
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
end
@@ -70,7 +70,7 @@ describe API::V3::Templates, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
+ expect(json_response.size).to eq(12)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
end
@@ -98,7 +98,7 @@ describe API::V3::Templates, api: true do
let(:license_type) { 'mit' }
it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
it 'replaces placeholder values' do
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
new file mode 100644
index 00000000000..9233e9621bf
--- /dev/null
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -0,0 +1,232 @@
+require 'spec_helper'
+
+describe API::V3::Triggers do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:trigger_token) { 'secure_token' }
+ let!(:project) { create(:project, :repository, creator: user) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+ let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
+ let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+
+ describe 'POST /projects/:project_id/trigger' do
+ let!(:project2) { create(:project) }
+ let(:options) do
+ {
+ token: trigger_token
+ }
+ end
+
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ end
+
+ context 'Handles errors' do
+ it 'returns bad request if token is missing' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master'
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns not found if project is not found' do
+ post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master')
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns unauthorized if token is for different project' do
+ post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'Have a commit' do
+ let(:pipeline) { project.pipelines.last }
+
+ it 'creates builds' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
+ end
+
+ it 'returns bad request with no builds created if there\'s no commit for that ref' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('No builds created')
+ end
+
+ context 'Validates variables' do
+ let(:variables) do
+ { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+ end
+
+ it 'validates variables to be a hash' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('variables is invalid')
+ end
+
+ it 'validates variables needs to be a map of key-valued strings' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
+ end
+
+ it 'creates trigger request with variables' do
+ post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
+ expect(response).to have_http_status(201)
+ pipeline.builds.reload
+ expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+ end
+ end
+ end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'returns list of triggers' do
+ get v3_api("/projects/#{project.id}/triggers", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_a(Array)
+ expect(json_response[0]).to have_key('token')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return triggers list' do
+ get v3_api("/projects/#{project.id}/triggers", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return triggers list' do
+ get v3_api("/projects/#{project.id}/triggers")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'returns trigger details' do
+ get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
+ get v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return triggers list' do
+ get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return triggers list' do
+ get v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'creates trigger' do
+ expect do
+ post v3_api("/projects/#{project.id}/triggers", user)
+ end.to change{project.triggers.count}.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to be_a(Hash)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not create trigger' do
+ post v3_api("/projects/#{project.id}/triggers", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not create trigger' do
+ post v3_api("/projects/#{project.id}/triggers")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'deletes trigger' do
+ expect do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{project.triggers.count}.by(-1)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not delete trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index 5020ef18a3a..17bbb0b53c1 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -186,4 +186,81 @@ describe API::V3::Users, api: true do
expect(response).to have_http_status(404)
end
end
+
+ describe 'GET /users/:id/events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
+
+ before do
+ project.add_user(user, :developer)
+ EventCreateService.new.leave_note(note, user)
+ end
+
+ context "as a user than cannot see the event's project" do
+ it 'returns no events' do
+ other_user = create(:user)
+
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user than can see the event's project" do
+ context 'joined event' do
+ it 'returns the "joined" event' do
+ get v3_api("/users/#{user.id}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+
+ comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_event['project_id'].to_i).to eq(project.id)
+ expect(comment_event['author_username']).to eq(user.username)
+ expect(comment_event['note']['id']).to eq(note.id)
+ expect(comment_event['note']['body']).to eq('What an awesome day!')
+
+ joined_event = json_response.find { |e| e['action_name'] == 'joined' }
+
+ expect(joined_event['project_id'].to_i).to eq(project.id)
+ expect(joined_event['author_username']).to eq(user.username)
+ expect(joined_event['author']['name']).to eq(user.name)
+ end
+ end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+ let(:third_note) { create(:note_on_issue, project: project) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note, third_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get v3_api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_events[0]['target_id']).to eq(third_note.id)
+ expect(comment_events[1]['target_id']).to eq(second_note.id)
+ expect(comment_events[2]['target_id']).to eq(note.id)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 769f04c5057..0c1413119e0 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -152,8 +152,9 @@ describe API::Variables, api: true do
it 'deletes variable' do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response).to have_http_status(204)
end.to change{project.variables.count}.by(-1)
- expect(response).to have_http_status(200)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index d85afdeab42..c879f37f50d 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Ci::API::Builds do
include ApiHelpers
- let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
+ let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
let(:last_update) { nil }
@@ -81,8 +81,8 @@ describe Ci::API::Builds do
expect(runner.reload.platform).to eq("darwin")
expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true }
)
end
@@ -182,9 +182,9 @@ describe Ci::API::Builds do
expect(response).to have_http_status(201)
expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+ { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
{ "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
{ "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
@@ -630,6 +630,7 @@ describe Ci::API::Builds do
context 'with an expire date' do
let!(:artifacts) { file_upload }
+ let(:default_artifacts_expire_in) {}
let(:post_data) do
{ 'file.path' => artifacts.path,
@@ -638,6 +639,9 @@ describe Ci::API::Builds do
end
before do
+ stub_application_setting(
+ default_artifacts_expire_in: default_artifacts_expire_in)
+
post(post_url, post_data, headers_with_token)
end
@@ -648,7 +652,8 @@ describe Ci::API::Builds do
build.reload
expect(response).to have_http_status(201)
expect(json_response['artifacts_expire_at']).not_to be_empty
- expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+ expect(build.artifacts_expire_at).
+ to be_within(5.minutes).of(7.days.from_now)
end
end
@@ -661,6 +666,32 @@ describe Ci::API::Builds do
expect(json_response['artifacts_expire_at']).to be_nil
expect(build.artifacts_expire_at).to be_nil
end
+
+ context 'with application default' do
+ context 'default to 5 days' do
+ let(:default_artifacts_expire_in) { '5 days' }
+
+ it 'sets to application default' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at'])
+ .not_to be_empty
+ expect(build.artifacts_expire_at)
+ .to be_within(5.minutes).of(5.days.from_now)
+ end
+ end
+
+ context 'default to 0' do
+ let(:default_artifacts_expire_in) { '0' }
+
+ it 'does not set expire_in' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at']).to be_nil
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index bd55934d0c8..d50cdfdc2d6 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -18,6 +18,7 @@ describe Ci::API::Runners do
it 'creates runner with default values' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be true
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
end
end
@@ -41,7 +42,7 @@ describe Ci::API::Runners do
it 'creates runner' do
expect(response).to have_http_status 201
- expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
+ expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
@@ -74,6 +75,8 @@ describe Ci::API::Runners do
it 'creates runner' do
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
+ expect(Ci::Runner.first.token).not_to eq(registration_token)
+ expect(Ci::Runner.first.token).not_to eq(project.runners_token)
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 87786e85621..006d6a6af1c 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do
end
context "when the user is blocked" do
- it "responds with status 404" do
+ it "responds with status 401" do
user.block
project.team << [user, :master]
download(path, env) do |response|
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ it "responds with status 401 for unknown projects (no project existence information leak)" do
+ user.block
+
+ download('doesnt/exist.git', env) do |response|
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index c0e7bab8199..5d495bc9e7d 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -25,11 +25,9 @@ describe 'Git LFS API and storage' do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
],
'operation' => 'upload'
}
@@ -53,11 +51,9 @@ describe 'Git LFS API and storage' do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
],
'operation' => 'upload'
}
@@ -374,11 +370,12 @@ describe 'Git LFS API and storage' do
describe 'download' do
let(:project) { create(:empty_project) }
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
@@ -393,16 +390,20 @@ describe 'Git LFS API and storage' do
end
it 'with href to download' do
- expect(json_response).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
}
}
- }])
+ ]
+ })
end
end
@@ -417,24 +418,29 @@ describe 'Git LFS API and storage' do
end
it 'with href to download' do
- expect(json_response).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
}
- }])
+ ]
+ })
end
end
context 'when downloading a lfs object that does not exist' do
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
+ 'size' => 1575078 }
+ ]
}
end
@@ -443,27 +449,30 @@ describe 'Git LFS API and storage' do
end
it 'with an 404 for specific object' do
- expect(json_response).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
}
- }])
+ ]
+ })
end
end
context 'when downloading one new and one existing lfs object' do
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
]
}
end
@@ -477,23 +486,28 @@ describe 'Git LFS API and storage' do
end
it 'responds with upload hypermedia link for the new object' do
- expect(json_response).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
}
}
- }])
+ ]
+ })
end
end
end
@@ -597,17 +611,21 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200 and href to download' do
- expect(json_response).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'authenticated' => true,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => {}
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'authenticated' => true,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => {}
+ }
}
}
- }])
+ ]
+ })
end
end
@@ -626,11 +644,12 @@ describe 'Git LFS API and storage' do
describe 'upload' do
let(:project) { create(:project, :public) }
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
@@ -665,11 +684,12 @@ describe 'Git LFS API and storage' do
context 'when pushing a lfs object that does not exist' do
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
+ 'size' => 1575078 }
+ ]
}
end
@@ -688,14 +708,13 @@ describe 'Git LFS API and storage' do
context 'when pushing one new and one existing lfs object' do
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
]
}
end
@@ -789,11 +808,12 @@ describe 'Git LFS API and storage' do
let(:project) { create(:empty_project) }
let(:authorization) { authorize_user }
let(:body) do
- { 'operation' => 'other',
+ {
+ 'operation' => 'other',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
new file mode 100644
index 00000000000..5206634bca5
--- /dev/null
+++ b/spec/requests/openid_connect_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe 'OpenID Connect requests' do
+ include ApiHelpers
+
+ let(:user) { create :user }
+ let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
+ let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
+
+ def request_access_token
+ login_as user
+
+ post '/oauth/token',
+ grant_type: 'authorization_code',
+ code: access_grant.token,
+ redirect_uri: application.redirect_uri,
+ client_id: application.uid,
+ client_secret: application.secret
+ end
+
+ def request_user_info
+ get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
+ end
+
+ def hashed_subject
+ Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
+ end
+
+ context 'Application without OpenID scope' do
+ let(:application) { create :oauth_application, scopes: 'api' }
+
+ it 'token response does not include an ID token' do
+ request_access_token
+
+ expect(json_response).to include 'access_token'
+ expect(json_response).not_to include 'id_token'
+ end
+
+ it 'userinfo response is unauthorized' do
+ request_user_info
+
+ expect(response).to have_http_status 403
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'Application with OpenID scope' do
+ let(:application) { create :oauth_application, scopes: 'openid' }
+
+ it 'token response includes an ID token' do
+ request_access_token
+
+ expect(json_response).to include 'id_token'
+ end
+
+ context 'UserInfo payload' do
+ let(:user) do
+ create(
+ :user,
+ name: 'Alice',
+ username: 'alice',
+ emails: [private_email, public_email],
+ email: private_email.email,
+ public_email: public_email.email,
+ website_url: 'https://example.com',
+ avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
+ )
+ end
+
+ let(:public_email) { build :email, email: 'public@example.com' }
+ let(:private_email) { build :email, email: 'private@example.com' }
+
+ it 'includes all user information' do
+ request_user_info
+
+ expect(json_response).to eq({
+ 'sub' => hashed_subject,
+ 'name' => 'Alice',
+ 'nickname' => 'alice',
+ 'email' => 'public@example.com',
+ 'email_verified' => true,
+ 'website' => 'https://example.com',
+ 'profile' => 'http://localhost/alice',
+ 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
+ })
+ end
+ end
+
+ context 'ID token payload' do
+ before do
+ request_access_token
+ @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
+ end
+
+ it 'includes the Gitlab root URL' do
+ expect(@payload['iss']).to eq Gitlab.config.gitlab.url
+ end
+
+ it 'includes the hashed user ID' do
+ expect(@payload['sub']).to eq hashed_subject
+ end
+
+ it 'includes the time of the last authentication' do
+ expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i
+ end
+
+ it 'does not include any unknown properties' do
+ expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
+ end
+ end
+
+ context 'when user is blocked' do
+ it 'returns authentication error' do
+ access_grant
+ user.block
+
+ expect do
+ request_access_token
+ end.to throw_symbol :warden
+ end
+ end
+
+ context 'when user is ldap_blocked' do
+ it 'returns authentication error' do
+ access_grant
+ user.ldap_block
+
+ expect do
+ request_access_token
+ end.to throw_symbol :warden
+ end
+ end
+ end
+end
diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb
new file mode 100644
index 00000000000..2c3bc08f1a1
--- /dev/null
+++ b/spec/routing/openid_connect_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
+# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
+# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
+describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
+ it "to #provider" do
+ expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider')
+ end
+
+ it "to #webfinger" do
+ expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger')
+ end
+
+ it "to #keys" do
+ expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
+ end
+end
+
+# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+# POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do
+ it "to #show" do
+ expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+ end
+
+ it "to #show" do
+ expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a5bc62ef6c2..4baccacd448 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -120,7 +120,6 @@ describe 'project routing' do
end
end
- # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
# members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
# issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
# merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
@@ -128,7 +127,7 @@ describe 'project routing' do
# milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
# commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
describe Projects::AutocompleteSourcesController, 'routing' do
- [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+ [:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
@@ -431,12 +430,22 @@ describe 'project routing' do
end
end
- # project_notes GET /:project_id/notes(.:format) notes#index
- # POST /:project_id/notes(.:format) notes#create
- # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy
+ # project_noteable_notes GET /:project_id/noteable/:target_type/:target_id/notes notes#index
+ # POST /:project_id/notes(.:format) notes#create
+ # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy
describe Projects::NotesController, 'routing' do
+ it 'to #index' do
+ expect(get('/gitlab/gitlabhq/noteable/issue/1/notes')).to route_to(
+ 'projects/notes#index',
+ namespace_id: 'gitlab',
+ project_id: 'gitlabhq',
+ target_type: 'issue',
+ target_id: '1'
+ )
+ end
+
it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
+ let(:actions) { [:create, :destroy] }
let(:controller) { 'notes' }
end
end
diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb
new file mode 100644
index 00000000000..381d7871a40
--- /dev/null
+++ b/spec/rubocop/cop/custom_error_class_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/custom_error_class'
+
+describe RuboCop::Cop::CustomErrorClass do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when a class has a body' do
+ it 'does nothing' do
+ inspect_source(cop, 'class CustomError < StandardError; def foo; end; end')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'when a class has no explicit superclass' do
+ it 'does nothing' do
+ inspect_source(cop, 'class CustomError; end')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'when a class has a superclass that does not end in Error' do
+ it 'does nothing' do
+ inspect_source(cop, 'class CustomError < BasicObject; end')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'when a class is empty and inherits from a class ending in Error' do
+ context 'when the class is on a single line' do
+ let(:source) do
+ <<-SOURCE
+ module Foo
+ class CustomError < Bar::Baz::BaseError; end
+ end
+ SOURCE
+ end
+
+ let(:expected) do
+ <<-EXPECTED
+ module Foo
+ CustomError = Class.new(Bar::Baz::BaseError)
+ end
+ EXPECTED
+ end
+
+ it 'registers an offense' do
+ expected_highlights = source.split("\n")[1].strip
+
+ inspect_source(cop, source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([2])
+ expect(cop.highlights).to contain_exactly(expected_highlights)
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
+
+ expect(autocorrected).to eq(expected)
+ end
+ end
+
+ context 'when the class is on multiple lines' do
+ let(:source) do
+ <<-SOURCE
+ module Foo
+ class CustomError < Bar::Baz::BaseError
+ end
+ end
+ SOURCE
+ end
+
+ let(:expected) do
+ <<-EXPECTED
+ module Foo
+ CustomError = Class.new(Bar::Baz::BaseError)
+ end
+ EXPECTED
+ end
+
+ it 'registers an offense' do
+ expected_highlights = source.split("\n")[1..2].join("\n").strip
+
+ inspect_source(cop, source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([2])
+ expect(cop.highlights).to contain_exactly(expected_highlights)
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb')
+
+ expect(autocorrected).to eq(expected)
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
new file mode 100644
index 00000000000..19a5718b0b1
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_concurrent_index'
+
+describe RuboCop::Cop::Migration::AddConcurrentIndex do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when add_concurrent_index is used inside a change method' do
+ inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when add_concurrent_index is used inside an up method' do
+ inspect_source(cop, 'def up; add_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index bb26513103d..b91234ddb1e 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -72,7 +72,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
shared_examples 'a pullable and pushable' do
it_behaves_like 'a accessible' do
- let(:actions) { ['pull', 'push'] }
+ let(:actions) { %w(pull push) }
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 305278843f5..22115c6566d 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -43,8 +43,8 @@ describe Boards::Issues::ListService, services: true do
described_class.new(project, user, params).execute
end
- context 'sets default order to priority' do
- it 'returns opened issues when list id is missing' do
+ context 'issues are ordered by priority' do
+ it 'returns opened issues when list_id is missing' do
params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 77f75167b3d..727ea04ea5c 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -78,8 +78,10 @@ describe Boards::Issues::MoveService, services: true do
end
context 'when moving to same list' do
- let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
@@ -90,6 +92,18 @@ describe Boards::Issues::MoveService, services: true do
expect(issue.reload.labels).to contain_exactly(bug, development)
end
+
+ it 'sorts issues' do
+ [issue, issue1, issue2].each do |issue|
+ issue.move_to_end && issue.save!
+ end
+
+ params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ceaca96e25b..a969829a63e 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -79,66 +79,53 @@ describe Ci::CreatePipelineService, services: true do
context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
- let(:messageFlip) { "some message[skip ci]" }
- let(:capMessage) { "some message[CI SKIP]" }
- let(:capMessageFlip) { "some message[SKIP CI]" }
+
+ ci_messages = [
+ "some message[ci skip]",
+ "some message[skip ci]",
+ "some message[CI SKIP]",
+ "some message[SKIP CI]",
+ "some message[ci_skip]",
+ "some message[skip_ci]",
+ "some message[ci-skip]",
+ "some message[skip-ci]"
+ ]
before do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
end
- it "skips builds creation if there is [ci skip] tag in commit message" do
- commits = [{ message: message }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
+ ci_messages.each do |ci_message|
+ it "skips builds creation if the commit message is #{ci_message}" do
+ commits = [{ message: ci_message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "skips builds creation if there is [skip ci] tag in commit message" do
- commits = [{ message: messageFlip }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
end
- it "skips builds creation if there is [CI SKIP] tag in commit message" do
- commits = [{ message: capMessage }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
+ it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
- it "skips builds creation if there is [SKIP CI] tag in commit message" do
- commits = [{ message: capMessageFlip }]
+ commits = [{ message: "some message" }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
+ expect(pipeline.builds.first.name).to eq("rspec")
end
- it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+ it "does not skip builds creation if the commit message is nil" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil }
- commits = [{ message: "some message" }]
+ commits = [{ message: nil }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
@@ -213,7 +200,7 @@ describe Ci::CreatePipelineService, services: true do
context 'with environment' do
before do
- config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+ config = YAML.dump(deploy: { environment: { name: "review/$CI_COMMIT_REF_NAME" }, script: 'ls' })
stub_ci_pipeline_yaml_file(config)
end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index d8c443d29d5..5e68343784d 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -13,8 +13,22 @@ describe Ci::CreateTriggerRequestService, services: true do
context 'valid params' do
subject { service.execute(project, trigger, 'master') }
- it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
- it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+ context 'without owner' do
+ it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+ it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+ end
+
+ context 'with owner' do
+ let(:owner) { create(:user) }
+ let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+
+ it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
+ it { expect(subject.pipeline.user).to eq(owner) }
+ it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+ it { expect(subject.builds.first.user).to eq(owner) }
+ end
end
context 'no commit for ref' do
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
deleted file mode 100644
index b3e0a7b9b58..00000000000
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-module Ci
- describe ImageForBuildService, services: true do
- let(:service) { ImageForBuildService.new }
- let(:project) { FactoryGirl.create(:empty_project) }
- let(:commit_sha) { '01234567890123456789' }
- let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
- let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
-
- describe '#execute' do
- before { build }
-
- context 'branch name' do
- before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) }
- before { build.run! }
- let(:image) { service.execute(project, ref: 'master') }
-
- it { expect(image).to be_kind_of(OpenStruct) }
- it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
- it { expect(image.name).to eq('build-running.svg') }
- end
-
- context 'unknown branch name' do
- let(:image) { service.execute(project, ref: 'feature') }
-
- it { expect(image).to be_kind_of(OpenStruct) }
- it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
- it { expect(image.name).to eq('build-unknown.svg') }
- end
-
- context 'commit sha' do
- before { build.run! }
- let(:image) { service.execute(project, sha: build.sha) }
-
- it { expect(image).to be_kind_of(OpenStruct) }
- it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
- it { expect(image.name).to eq('build-running.svg') }
- end
-
- context 'unknown commit sha' do
- let(:image) { service.execute(project, sha: '0000000') }
-
- it { expect(image).to be_kind_of(OpenStruct) }
- it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
- it { expect(image.name).to eq('build-unknown.svg') }
- end
- end
- end
-end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ef2ddc4b1d7..d93616c4f50 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::ProcessPipelineService, :services do
+describe Ci::ProcessPipelineService, '#execute', :services do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
@@ -12,381 +12,518 @@ describe Ci::ProcessPipelineService, :services do
project.add_developer(user)
end
- describe '#execute' do
- context 'start queuing next builds' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
- end
+ context 'when simple pipeline is defined' do
+ before do
+ create_build('linux', stage_idx: 0)
+ create_build('mac', stage_idx: 0)
+ create_build('rspec', stage_idx: 1)
+ create_build('rubocop', stage_idx: 1)
+ create_build('deploy', stage_idx: 2)
+ end
- it 'processes a pipeline' do
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(2)
+ it 'processes a pipeline' do
+ expect(process_pipeline).to be_truthy
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(4)
+ succeed_pending
+
+ expect(builds.success.count).to eq(2)
+ expect(process_pipeline).to be_truthy
+
+ succeed_pending
+
+ expect(builds.success.count).to eq(4)
+ expect(process_pipeline).to be_truthy
+
+ succeed_pending
+
+ expect(builds.success.count).to eq(5)
+ expect(process_pipeline).to be_falsey
+ end
+
+ it 'does not process pipeline if existing stage is running' do
+ expect(process_pipeline).to be_truthy
+ expect(builds.pending.count).to eq(2)
+
+ expect(process_pipeline).to be_falsey
+ expect(builds.pending.count).to eq(2)
+ end
+ end
+
+ context 'custom stage with first job allowed to fail' do
+ before do
+ create_build('clean_job', stage_idx: 0, allow_failure: true)
+ create_build('test_job', stage_idx: 1, allow_failure: true)
+ end
+ it 'automatically triggers a next stage when build finishes' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_statuses).to eq ['pending']
+
+ fail_running_or_pending
+
+ expect(builds_statuses).to eq %w(failed pending)
+ end
+ end
+
+ context 'when optional manual actions are defined' do
+ before do
+ create_build('build', stage_idx: 0)
+ create_build('test', stage_idx: 1)
+ create_build('test_failure', stage_idx: 2, when: 'on_failure')
+ create_build('deploy', stage_idx: 3)
+ create_build('production', stage_idx: 3, when: 'manual', allow_failure: true)
+ create_build('cleanup', stage_idx: 4, when: 'always')
+ create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true)
+ end
+
+ context 'when builds are successful' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(5)
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- expect(process_pipeline).to be_falsey
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production)
+ expect(builds_statuses).to eq %w(success success pending manual)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production cleanup clear:cache)
+ expect(builds_statuses).to eq %w(success success success manual pending manual)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success success success manual success manual)
+ expect(pipeline.reload.status).to eq 'success'
end
+ end
- it 'does not process pipeline if existing stage is running' do
+ context 'when test job fails' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- expect(builds.pending.count).to eq(2)
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- expect(process_pipeline).to be_falsey
- expect(builds.pending.count).to eq(2)
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure)
+ expect(builds_statuses).to eq %w(success failed pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed success pending)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success failed success success)
+ expect(pipeline.reload.status).to eq 'failed'
end
end
- context 'custom stage with first job allowed to fail' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
- create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+ context 'when test and test_failure jobs fail' do
+ it 'properly processes the pipeline' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure)
+ expect(builds_statuses).to eq %w(success failed pending)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed failed pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed failed success)
+ expect(pipeline.reload.status).to eq('failed')
end
+ end
- it 'automatically triggers a next stage when build finishes' do
+ context 'when deploy job fails' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- expect(builds.pluck(:status)).to contain_exactly('pending')
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
- pipeline.builds.running_or_pending.each(&:drop)
- expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production)
+ expect(builds_statuses).to eq %w(success success pending manual)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production cleanup)
+ expect(builds_statuses).to eq %w(success success failed manual pending)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success success failed manual success)
+ expect(pipeline.reload).to be_failed
end
end
- context 'properly creates builds when "when" is defined' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
- create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
- create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
- create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
- end
+ context 'when build is canceled in the second stage' do
+ it 'does not schedule builds after build has been canceled' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
- context 'when builds are successful' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('success')
- end
- end
+ succeed_running_or_pending
- context 'when test job fails' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
- end
+ expect(builds.running_or_pending).not_to be_empty
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- context 'when test and test_failure jobs fail' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
- end
+ cancel_running_or_pending
- context 'when deploy job fails' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ expect(builds.running_or_pending).to be_empty
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success canceled]
+ expect(pipeline.reload).to be_canceled
end
+ end
- context 'when build is canceled in the second stage' do
- it 'does not schedule builds after build has been canceled' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ context 'when listing optional manual actions' do
+ it 'returns only for skipped builds' do
+ # currently all builds are created
+ expect(process_pipeline).to be_truthy
+ expect(manual_actions).to be_empty
- expect(builds.running_or_pending).not_to be_empty
+ # succeed stage build
+ succeed_running_or_pending
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:cancel)
+ expect(manual_actions).to be_empty
- expect(builds.running_or_pending).to be_empty
- expect(pipeline.reload.status).to eq('canceled')
- end
- end
+ # succeed stage test
+ succeed_running_or_pending
- context 'when listing manual actions' do
- it 'returns only for skipped builds' do
- # currently all builds are created
- expect(process_pipeline).to be_truthy
- expect(manual_actions).to be_empty
+ expect(manual_actions).to be_one # production
- # succeed stage build
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_empty
+ # succeed stage deploy
+ succeed_running_or_pending
- # succeed stage test
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_one # production
+ expect(manual_actions).to be_many # production and clear cache
+ end
+ end
+ end
+
+ context 'when there are manual action in earlier stages' do
+ context 'when first stage has only optional manual actions' do
+ before do
+ create_build('build', stage_idx: 0, when: 'manual', allow_failure: true)
+ create_build('check', stage_idx: 1)
+ create_build('test', stage_idx: 2)
- # succeed stage deploy
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_many # production and clear cache
- end
+ process_pipeline
+ end
+
+ it 'starts from the second stage' do
+ expect(all_builds_statuses).to eq %w[manual pending created]
end
end
- context 'when there are manual/on_failure jobs in earlier stages' do
+ context 'when second stage has only optional manual actions' do
before do
- builds
+ create_build('check', stage_idx: 0)
+ create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
+ create_build('test', stage_idx: 2)
+
process_pipeline
- builds.each(&:reload)
end
- context 'when first stage has only manual jobs' do
- let(:builds) do
- [create_build('build', 0, 'manual'),
- create_build('check', 1),
- create_build('test', 2)]
- end
+ it 'skips second stage and continues on third stage' do
+ expect(all_builds_statuses).to eq(%w[pending created created])
- it 'starts from the second stage' do
- expect(builds.map(&:status)).to eq(%w[skipped pending created])
- end
+ builds.first.success
+
+ expect(all_builds_statuses).to eq(%w[success manual pending])
end
+ end
+ end
+
+ context 'when blocking manual actions are defined' do
+ before do
+ create_build('code:test', stage_idx: 0)
+ create_build('staging:deploy', stage_idx: 1, when: 'manual')
+ create_build('staging:test', stage_idx: 2, when: 'on_success')
+ create_build('production:deploy', stage_idx: 3, when: 'manual')
+ create_build('production:test', stage_idx: 4, when: 'always')
+ end
- context 'when second stage has only manual jobs' do
- let(:builds) do
- [create_build('check', 0),
- create_build('build', 1, 'manual'),
- create_build('test', 2)]
- end
+ context 'when first stage succeeds' do
+ it 'blocks pipeline on stage with first manual action' do
+ process_pipeline
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- builds.first.success
- builds.each(&:reload)
+ succeed_running_or_pending
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ expect(builds_names).to eq %w[code:test staging:deploy]
+ expect(builds_statuses).to eq %w[success manual]
+ expect(pipeline.reload).to be_manual
end
+ end
+
+ context 'when first stage fails' do
+ it 'does not take blocking action into account' do
+ process_pipeline
+
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- context 'when second stage has only on_failure jobs' do
- let(:builds) do
- [create_build('check', 0),
- create_build('build', 1, 'on_failure'),
- create_build('test', 2)]
- end
+ fail_running_or_pending
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ expect(builds_names).to eq %w[code:test production:test]
+ expect(builds_statuses).to eq %w[failed pending]
- builds.first.success
- builds.each(&:reload)
+ succeed_running_or_pending
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ expect(builds_statuses).to eq %w[failed success]
+ expect(pipeline.reload).to be_failed
end
end
- context 'when failed build in the middle stage is retried' do
- context 'when failed build is the only unsuccessful build in the stage' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
- end
+ context 'when pipeline is promoted sequentially up to the end' do
+ it 'properly processes entire pipeline' do
+ process_pipeline
+
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy]
+ expect(builds_statuses).to eq %w[success manual]
+ expect(pipeline.reload).to be_manual
+
+ play_manual_action('staging:deploy')
+
+ expect(builds_statuses).to eq %w[success pending]
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test]
+ expect(builds_statuses).to eq %w[success success pending]
+
+ succeed_running_or_pending
- it 'does trigger builds in the next stage' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy]
+ expect(builds_statuses).to eq %w[success success success manual]
- pipeline.builds.running_or_pending.each(&:success)
+ expect(pipeline.reload).to be_manual
+ expect(pipeline.reload).to be_blocked
+ expect(pipeline.reload).not_to be_active
+ expect(pipeline.reload).not_to be_complete
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ play_manual_action('production:deploy')
- pipeline.builds.find_by(name: 'test:1').success
- pipeline.builds.find_by(name: 'test:2').drop
+ expect(builds_statuses).to eq %w[success success success pending]
+ expect(pipeline.reload).to be_running
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ succeed_running_or_pending
- Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy production:test]
+ expect(builds_statuses).to eq %w[success success success success pending]
+ expect(pipeline.reload).to be_running
- expect(builds.pluck(:name)).to contain_exactly(
- 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
- end
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy production:test]
+ expect(builds_statuses).to eq %w[success success success success success]
+ expect(pipeline.reload).to be_success
end
end
+ end
- context 'when there are builds that are not created yet' do
- let(:pipeline) do
- create(:ci_pipeline, config: config)
- end
+ context 'when second stage has only on_failure jobs' do
+ before do
+ create_build('check', stage_idx: 0)
+ create_build('build', stage_idx: 1, when: 'on_failure')
+ create_build('test', stage_idx: 2)
- let(:config) do
- { rspec: { stage: 'test', script: 'rspec' },
- deploy: { stage: 'deploy', script: 'rsync' } }
- end
+ process_pipeline
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(all_builds_statuses).to eq(%w[pending created created])
+ builds.first.success
+
+ expect(all_builds_statuses).to eq(%w[success skipped pending])
+ end
+ end
+
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
before do
- create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+ create_build('build:1', stage_idx: 0)
+ create_build('build:2', stage_idx: 0)
+ create_build('test:1', stage_idx: 1)
+ create_build('test:2', stage_idx: 1)
+ create_build('deploy:1', stage_idx: 2)
+ create_build('deploy:2', stage_idx: 2)
end
- it 'processes the pipeline' do
- # Currently we have five builds with state created
- #
- expect(builds.count).to eq(0)
- expect(all_builds.count).to eq(2)
+ it 'does trigger builds in the next stage' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build:1', 'build:2']
- # Process builds service will enqueue builds from the first stage.
- #
- process_pipeline
+ succeed_running_or_pending
- expect(builds.count).to eq(2)
- expect(all_builds.count).to eq(2)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
- # When builds succeed we will enqueue remaining builds.
- #
- # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
- # additional build from `.gitlab-ci.yml`).
- #
- succeed_pending
- process_pipeline
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
- expect(builds.success.count).to eq(2)
- expect(builds.pending.count).to eq(1)
- expect(all_builds.count).to eq(4)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
- # When pending build succeeds in stage test, we enqueue deploy stage.
- #
- succeed_pending
- process_pipeline
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
- expect(builds.pending.count).to eq(1)
- expect(builds.success.count).to eq(3)
- expect(all_builds.count).to eq(4)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2',
+ 'test:2', 'deploy:1', 'deploy:2']
+ end
+ end
+ end
- # When the last one succeeds we have 4 successful builds.
- #
- succeed_pending
- process_pipeline
+ context 'when there are builds that are not created yet' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: config)
+ end
- expect(builds.success.count).to eq(4)
- expect(all_builds.count).to eq(4)
- end
+ let(:config) do
+ { rspec: { stage: 'test', script: 'rspec' },
+ deploy: { stage: 'deploy', script: 'rsync' } }
+ end
+
+ before do
+ create_build('linux', stage: 'build', stage_idx: 0)
+ create_build('mac', stage: 'build', stage_idx: 0)
+ end
+
+ it 'processes the pipeline' do
+ # Currently we have five builds with state created
+ #
+ expect(builds.count).to eq(0)
+ expect(all_builds.count).to eq(2)
+
+ # Process builds service will enqueue builds from the first stage.
+ #
+ process_pipeline
+
+ expect(builds.count).to eq(2)
+ expect(all_builds.count).to eq(2)
+
+ # When builds succeed we will enqueue remaining builds.
+ #
+ # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
+ # additional build from `.gitlab-ci.yml`).
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.success.count).to eq(2)
+ expect(builds.pending.count).to eq(1)
+ expect(all_builds.count).to eq(4)
+
+ # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.pending.count).to eq(1)
+ expect(builds.success.count).to eq(3)
+ expect(all_builds.count).to eq(4)
+
+ # When the last one succeeds we have 4 successful builds.
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.success.count).to eq(4)
+ expect(all_builds.count).to eq(4)
end
end
+ def process_pipeline
+ described_class.new(pipeline.project, user).execute(pipeline)
+ end
+
def all_builds
- pipeline.builds
+ pipeline.builds.order(:stage_idx, :id)
end
def builds
all_builds.where.not(status: [:created, :skipped])
end
- def process_pipeline
- described_class.new(pipeline.project, user).execute(pipeline)
+ def builds_names
+ builds.pluck(:name)
+ end
+
+ def builds_statuses
+ builds.pluck(:status)
+ end
+
+ def all_builds_statuses
+ all_builds.pluck(:status)
end
def succeed_pending
builds.pending.update_all(status: 'success')
end
- def manual_actions
- pipeline.manual_actions
+ def succeed_running_or_pending
+ pipeline.builds.running_or_pending.each(&:success)
+ end
+
+ def fail_running_or_pending
+ pipeline.builds.running_or_pending.each(&:drop)
+ end
+
+ def cancel_running_or_pending
+ pipeline.builds.running_or_pending.each(&:cancel)
+ end
+
+ def play_manual_action(name)
+ builds.find_by(name: name).play(user)
end
- def create_build(name, stage_idx, when_value = nil)
- create(:ci_build,
- :created,
- pipeline: pipeline,
- name: name,
- stage_idx: stage_idx,
- when: when_value)
+ delegate :manual_actions, to: :pipeline
+
+ def create_build(name, **opts)
+ create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
end
end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
deleted file mode 100644
index d9f774a1095..00000000000
--- a/spec/services/ci/register_build_service_spec.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-require 'spec_helper'
-
-module Ci
- describe RegisterBuildService, services: true do
- let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
- let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
- let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
- let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
-
- before do
- specific_runner.assign_to(project)
- end
-
- describe '#execute' do
- context 'runner follow tag list' do
- it "picks build with the same tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
- specific_runner.tag_list = ["linux"]
- expect(execute(specific_runner)).to eq(pending_build)
- end
-
- it "does not pick build with different tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
- specific_runner.tag_list = ["win32"]
- expect(execute(specific_runner)).to be_falsey
- end
-
- it "picks build without tag" do
- expect(execute(specific_runner)).to eq(pending_build)
- end
-
- it "does not pick build with tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
- expect(execute(specific_runner)).to be_falsey
- end
-
- it "pick build without tag" do
- specific_runner.tag_list = ["win32"]
- expect(execute(specific_runner)).to eq(pending_build)
- end
- end
-
- context 'deleted projects' do
- before do
- project.update(pending_delete: true)
- end
-
- context 'for shared runners' do
- before do
- project.update(shared_runners_enabled: true)
- end
-
- it 'does not pick a build' do
- expect(execute(shared_runner)).to be_nil
- end
- end
-
- context 'for specific runner' do
- it 'does not pick a build' do
- expect(execute(specific_runner)).to be_nil
- end
- end
- end
-
- context 'allow shared runners' do
- before do
- project.update(shared_runners_enabled: true)
- end
-
- context 'for multiple builds' do
- let!(:project2) { create :empty_project, shared_runners_enabled: true }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :empty_project, shared_runners_enabled: true }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
- let!(:build1_project1) { pending_build }
- let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
- let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
- let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
- let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 }
-
- it 'prefers projects without builds first' do
- # it gets for one build from each of the projects
- expect(execute(shared_runner)).to eq(build1_project1)
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
-
- # then it gets a second build from each of the projects
- expect(execute(shared_runner)).to eq(build2_project1)
- expect(execute(shared_runner)).to eq(build2_project2)
-
- # in the end the third build
- expect(execute(shared_runner)).to eq(build3_project1)
- end
-
- it 'equalises number of running builds' do
- # after finishing the first build for project 1, get a second build from the same project
- expect(execute(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(execute(shared_runner)).to eq(build2_project1)
-
- expect(execute(shared_runner)).to eq(build1_project2)
- build1_project2.reload.success
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
- expect(execute(shared_runner)).to eq(build3_project1)
- end
- end
-
- context 'shared runner' do
- let(:build) { execute(shared_runner) }
-
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(shared_runner) }
- end
-
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
-
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
- end
- end
-
- context 'disallow shared runners' do
- before do
- project.update(shared_runners_enabled: false)
- end
-
- context 'shared runner' do
- let(:build) { execute(shared_runner) }
-
- it { expect(build).to be_nil }
- end
-
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
-
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
- end
- end
-
- context 'disallow when builds are disabled' do
- before do
- project.update(shared_runners_enabled: true)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
- end
-
- context 'and uses shared runner' do
- let(:build) { execute(shared_runner) }
-
- it { expect(build).to be_nil }
- end
-
- context 'and uses specific runner' do
- let(:build) { execute(specific_runner) }
-
- it { expect(build).to be_nil }
- end
- end
-
- def execute(runner)
- described_class.new(runner).execute.build
- end
- end
- end
-end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
new file mode 100644
index 00000000000..62ba0b01339
--- /dev/null
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -0,0 +1,223 @@
+require 'spec_helper'
+
+module Ci
+ describe RegisterJobService, services: true do
+ let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
+ let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
+ let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
+
+ before do
+ specific_runner.assign_to(project)
+ end
+
+ describe '#execute' do
+ context 'runner follow tag list' do
+ it "picks build with the same tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["linux"]
+ expect(execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with different tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["win32"]
+ expect(execute(specific_runner)).to be_falsey
+ end
+
+ it "picks build without tag" do
+ expect(execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ expect(execute(specific_runner)).to be_falsey
+ end
+
+ it "pick build without tag" do
+ specific_runner.tag_list = ["win32"]
+ expect(execute(specific_runner)).to eq(pending_build)
+ end
+ end
+
+ context 'deleted projects' do
+ before do
+ project.update(pending_delete: true)
+ end
+
+ context 'for shared runners' do
+ before do
+ project.update(shared_runners_enabled: true)
+ end
+
+ it 'does not pick a build' do
+ expect(execute(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for specific runner' do
+ it 'does not pick a build' do
+ expect(execute(specific_runner)).to be_nil
+ end
+ end
+ end
+
+ context 'allow shared runners' do
+ before do
+ project.update(shared_runners_enabled: true)
+ end
+
+ context 'for multiple builds' do
+ let!(:project2) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_build }
+ let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 }
+
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(execute(shared_runner)).to eq(build1_project1)
+ expect(execute(shared_runner)).to eq(build1_project2)
+ expect(execute(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(execute(shared_runner)).to eq(build2_project1)
+ expect(execute(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(execute(shared_runner)).to eq(build3_project1)
+ end
+
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(execute(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(execute(shared_runner)).to eq(build2_project1)
+
+ expect(execute(shared_runner)).to eq(build1_project2)
+ build1_project2.reload.success
+ expect(execute(shared_runner)).to eq(build2_project2)
+ expect(execute(shared_runner)).to eq(build1_project3)
+ expect(execute(shared_runner)).to eq(build3_project1)
+ end
+ end
+
+ context 'shared runner' do
+ let(:build) { execute(shared_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ end
+
+ context 'specific runner' do
+ let(:build) { execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+
+ context 'disallow shared runners' do
+ before do
+ project.update(shared_runners_enabled: false)
+ end
+
+ context 'shared runner' do
+ let(:build) { execute(shared_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'specific runner' do
+ let(:build) { execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+
+ context 'disallow when builds are disabled' do
+ before do
+ project.update(shared_runners_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ context 'and uses shared runner' do
+ let(:build) { execute(shared_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'and uses specific runner' do
+ let(:build) { execute(specific_runner) }
+
+ it { expect(build).to be_nil }
+ end
+ end
+
+ context 'when first build is stalled' do
+ before do
+ pending_build.lock_version = 10
+ end
+
+ subject { described_class.new(specific_runner).execute }
+
+ context 'with multiple builds are in queue' do
+ let!(:other_build) { create :ci_build, pipeline: pipeline }
+
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ .and_return([pending_build, other_build])
+ end
+
+ it "receives second build from the queue" do
+ expect(subject).to be_valid
+ expect(subject.build).to eq(other_build)
+ end
+ end
+
+ context 'when single build is in queue' do
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ .and_return([pending_build])
+ end
+
+ it "does not receive any valid result" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context 'when there is no build in queue' do
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ .and_return([])
+ end
+
+ it "does not receive builds but result is valid" do
+ expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ end
+ end
+ end
+
+ def execute(runner)
+ described_class.new(runner).execute.build
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 93147870afe..8567817147b 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -10,22 +10,39 @@ describe Ci::RetryBuildService, :services do
described_class.new(project, user)
end
+ CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
+
+ REJECT_ACCESSORS =
+ %i[id status user token coverage trace runner artifacts_expire_at
+ artifacts_file artifacts_metadata artifacts_size created_at
+ updated_at started_at finished_at queued_at erased_by
+ erased_at].freeze
+
+ IGNORE_ACCESSORS =
+ %i[type lock_version target_url base_tags
+ commit_id deployments erased_by_id last_deployment project_id
+ runner_id tag_taggings taggings tags trigger_request_id
+ user_id].freeze
+
shared_examples 'build duplication' do
let(:build) do
- create(:ci_build, :failed, :artifacts, :erased, :trace,
- :queued, :coverage, pipeline: pipeline)
+ create(:ci_build, :failed, :artifacts_expired, :erased,
+ :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
+ :teardown_environment, :triggered, :trace,
+ description: 'some build', pipeline: pipeline)
end
- describe 'clone attributes' do
- described_class::CLONE_ATTRIBUTES.each do |attribute|
+ describe 'clone accessors' do
+ CLONE_ACCESSORS.each do |attribute|
it "clones #{attribute} build attribute" do
+ expect(new_build.send(attribute)).to be_present
expect(new_build.send(attribute)).to eq build.send(attribute)
end
end
end
- describe 'reject attributes' do
- described_class::REJECT_ATTRIBUTES.each do |attribute|
+ describe 'reject acessors' do
+ REJECT_ACCESSORS.each do |attribute|
it "does not clone #{attribute} build attribute" do
expect(new_build.send(attribute)).not_to eq build.send(attribute)
end
@@ -33,12 +50,20 @@ describe Ci::RetryBuildService, :services do
end
it 'has correct number of known attributes' do
- attributes =
- described_class::CLONE_ATTRIBUTES +
- described_class::IGNORE_ATTRIBUTES +
- described_class::REJECT_ATTRIBUTES
+ known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS
+
+ # :tag_list is a special case, this accessor does not exist
+ # in reflected associations, comes from `act_as_taggable` and
+ # we use it to copy tags, instead of reusing tags.
+ #
+ current_accessors =
+ Ci::Build.attribute_names.map(&:to_sym) +
+ Ci::Build.reflect_on_all_associations.map(&:name) +
+ [:tag_list]
+
+ current_accessors.uniq!
- expect(attributes.size).to eq build.attributes.size
+ expect(known_accessors).to contain_exactly(*current_accessors)
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index c0af8b8450a..5445b65f4e8 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -69,35 +69,95 @@ describe Ci::RetryPipelineService, '#execute', :services do
end
end
+ context 'when the last stage was skipepd' do
+ before do
+ create_build('build 1', :success, 0)
+ create_build('test 2', :failed, 1)
+ create_build('report 3', :skipped, 2)
+ create_build('report 4', :skipped, 2)
+ end
+
+ it 'retries builds only in the first stage' do
+ service.execute(pipeline)
+
+ expect(build('build 1')).to be_success
+ expect(build('test 2')).to be_pending
+ expect(build('report 3')).to be_created
+ expect(build('report 4')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
context 'when pipeline contains manual actions' do
- context 'when there is a canceled manual action in first stage' do
- before do
- create_build('rspec 1', :failed, 0)
- create_build('staging', :canceled, 0, :manual)
- create_build('rspec 2', :canceled, 1)
+ context 'when there are optional manual actions only' do
+ context 'when there is a canceled manual action in first stage' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('staging', :canceled, 0, when: :manual, allow_failure: true)
+ create_build('rspec 2', :canceled, 1)
+ end
+
+ it 'retries failed builds and marks subsequent for processing' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_manual
+ expect(build('rspec 2')).to be_created
+ expect(pipeline.reload).to be_running
+ end
end
+ end
- it 'retries builds failed builds and marks subsequent for processing' do
- service.execute(pipeline)
+ context 'when pipeline has blocking manual actions defined' do
+ context 'when pipeline retry should enqueue builds' do
+ before do
+ create_build('test', :failed, 0)
+ create_build('deploy', :canceled, 0, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, 1)
+ end
+
+ it 'retries failed builds' do
+ service.execute(pipeline)
+
+ expect(build('test')).to be_pending
+ expect(build('deploy')).to be_manual
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
- expect(build('rspec 1')).to be_pending
- expect(build('staging')).to be_skipped
- expect(build('rspec 2')).to be_created
- expect(pipeline.reload).to be_running
+ context 'when pipeline retry should block pipeline immediately' do
+ before do
+ create_build('test', :success, 0)
+ create_build('deploy:1', :success, 1, when: :manual, allow_failure: false)
+ create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, 2)
+ end
+
+ it 'reprocesses blocking manual action and blocks pipeline' do
+ service.execute(pipeline)
+
+ expect(build('deploy:1')).to be_success
+ expect(build('deploy:2')).to be_manual
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_blocked
+ end
end
end
context 'when there is a skipped manual action in last stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('staging', :skipped, 1, :manual)
+ create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true)
+ create_build('staging', :skipped, 1, when: :manual, allow_failure: true)
end
- it 'retries canceled job and skips manual action' do
+ it 'retries canceled job and reprocesses manual actions' do
service.execute(pipeline)
expect(build('rspec 1')).to be_pending
- expect(build('staging')).to be_skipped
+ expect(build('rspec 2')).to be_manual
+ expect(build('staging')).to be_created
expect(pipeline.reload).to be_running
end
end
@@ -105,7 +165,7 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when there is a created manual action in the last stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 1, :manual)
+ create_build('staging', :created, 1, when: :manual, allow_failure: true)
end
it 'retries canceled job and does not update the manual action' do
@@ -120,14 +180,14 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when there is a created manual action in the first stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 0, :manual)
+ create_build('staging', :created, 0, when: :manual, allow_failure: true)
end
- it 'retries canceled job and skipps the manual action' do
+ it 'retries canceled job and processes the manual action' do
service.execute(pipeline)
expect(build('rspec 1')).to be_pending
- expect(build('staging')).to be_skipped
+ expect(build('staging')).to be_manual
expect(pipeline.reload).to be_running
end
end
@@ -162,13 +222,12 @@ describe Ci::RetryPipelineService, '#execute', :services do
statuses.latest.find_by(name: name)
end
- def create_build(name, status, stage_num, on = 'on_success')
+ def create_build(name, status, stage_num, **opts)
create(:ci_build, name: name,
status: status,
stage: "stage_#{stage_num}",
stage_idx: stage_num,
- when: on,
- pipeline: pipeline) do |build|
+ pipeline: pipeline, **opts) do |build|
pipeline.update_status
end
end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 6fb4d517115..a883705bd45 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -9,7 +9,8 @@ describe CreateDeploymentService, services: true do
describe '#execute' do
let(:options) { nil }
let(:params) do
- { environment: 'production',
+ {
+ environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -83,10 +84,11 @@ describe CreateDeploymentService, services: true do
context 'for environment with invalid name' do
let(:params) do
- { environment: 'name,with,commas',
+ {
+ environment: 'name,with,commas',
ref: 'master',
tag: false,
- sha: '97de212e80737a608d939f648d959671fb0a0142',
+ sha: '97de212e80737a608d939f648d959671fb0a0142'
}
end
@@ -101,16 +103,17 @@ describe CreateDeploymentService, services: true do
context 'when variables are used' do
let(:params) do
- { environment: 'review-apps/$CI_BUILD_REF_NAME',
+ {
+ environment: 'review-apps/$CI_COMMIT_REF_NAME',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
options: {
- name: 'review-apps/$CI_BUILD_REF_NAME',
- url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
+ name: 'review-apps/$CI_COMMIT_REF_NAME',
+ url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com'
},
variables: [
- { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+ { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' }
]
}
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 2a0f00ce937..bd71618e6f4 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -150,6 +150,13 @@ describe GitPushService, services: true do
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
end
end
+
+ context "Sends System Push data" do
+ it "when pushing on a branch" do
+ expect(SystemHookPushWorker).to receive(:perform_async).with(@push_data, :push_hooks)
+ execute_service(project, user, @oldrev, @newrev, @ref )
+ end
+ end
end
describe "Updates git attributes" do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 14717a7455d..ec89b540e6a 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do
let!(:user) { create(:user) }
let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+ subject { service.execute }
+
describe 'visibility level restrictions' do
let!(:service) { described_class.new(user, group_params) }
- subject { service.execute }
-
context "create groups without restricted visibility level" do
it { is_expected.to be_persisted }
end
@@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do
let!(:group) { create(:group) }
let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
- subject { service.execute }
-
context 'as group owner' do
before { group.add_owner(user) }
@@ -40,4 +38,20 @@ describe Groups::CreateService, '#execute', services: true do
end
end
end
+
+ describe 'creating a mattermost team' do
+ let!(:params) { group_params.merge(create_chat_team: "true") }
+ let!(:service) { described_class.new(user, params) }
+
+ before do
+ Settings.mattermost['enabled'] = true
+ end
+
+ it 'create the chat team with the group' do
+ allow_any_instance_of(Mattermost::Team).to receive(:create)
+ .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
+
+ expect { subject }.to change { ChatTeam.count }.from(0).to(1)
+ end
+ end
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 32c2ed8cae7..98c560ffb26 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do
end
it { expect(Group.unscoped.all).not_to include(group) }
+ it { expect(Group.unscoped.all).not_to include(nested_group) }
it { expect(Project.unscoped.all).not_to include(project) }
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 09807e5d35b..1dd53236fbd 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -8,24 +8,34 @@ describe Issues::BuildService, services: true do
project.team << [user, :developer]
end
+ context 'for a single discussion' do
+ describe '#execute' do
+ let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
+ let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done")]) }
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
+
+ it 'references the noteable title in the issue title' do
+ issue = service.execute
+
+ expect(issue.title).to include('Hello world')
+ end
+
+ it 'adds the note content to the description' do
+ issue = service.execute
+
+ expect(issue.description).to include('Almost done')
+ end
+ end
+ end
+
context 'for discussions in a merge request' do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
- let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute }
-
- def position_on_line(line_number)
- Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: line_number,
- diff_refs: merge_request.diff_refs
- )
- end
+ let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute }
describe '#items_for_discussions' do
it 'has an item for each discussion' do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13))
- service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request)
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13)
+ service = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid)
service.execute
@@ -34,7 +44,7 @@ describe Issues::BuildService, services: true do
end
describe '#item_for_discussion' do
- let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) }
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
it 'mentions the author of the note' do
discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))])
@@ -47,11 +57,11 @@ describe Issues::BuildService, services: true do
"with a blockquote\n"\
"> That has a quote\n"\
">>>\n"
- note_result = "This is a string\n"\
- "> with a blockquote\n"\
- "> > That has a quote\n"
+ note_result = " > This is a string\n"\
+ " > > with a blockquote\n"\
+ " > > > That has a quote\n"
discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)])
- expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>")
+ expect(service.item_for_discussion(discussion)).to include(note_result)
end
end
@@ -66,7 +76,7 @@ describe Issues::BuildService, services: true do
it 'does not assign title when a title was given' do
issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
+ merge_request_to_resolve_discussions_of: merge_request,
title: 'What an issue').execute
expect(issue.title).to eq('What an issue')
@@ -74,7 +84,7 @@ describe Issues::BuildService, services: true do
it 'does not assign description when a description was given' do
issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
+ merge_request_to_resolve_discussions_of: merge_request,
description: 'Fix at your earliest conveignance').execute
expect(issue.description).to eq('Fix at your earliest conveignance')
@@ -82,7 +92,7 @@ describe Issues::BuildService, services: true do
describe 'with multiple discussions' do
before do
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, line_number: 15)
end
it 'mentions all the authors in the description' do
@@ -99,7 +109,7 @@ describe Issues::BuildService, services: true do
end
it 'mentions additional notes' do
- create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15))
+ create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, line_number: 15)
expect(issue.description).to include('(+2 comments)')
end
@@ -112,7 +122,7 @@ describe Issues::BuildService, services: true do
describe '#execute' do
it 'mentions the merge request in the description' do
- issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute
+ issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute
expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}")
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 6045d00ff09..776cbc4296b 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -140,46 +140,85 @@ describe Issues::CreateService, services: true do
it_behaves_like 'new issuable record that supports slash commands'
- context 'for a merge request' do
+ context 'resolving discussions' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
- let(:opts) { { merge_request_for_resolving_discussions: merge_request } }
before do
project.team << [user, :master]
end
- it 'resolves the discussion for the merge request' do
- described_class.new(project, user, opts).execute
- discussion.first_note.reload
+ describe 'for a single discussion' do
+ let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
- expect(discussion.resolved?).to be(true)
- end
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
- it 'added a system note to the discussion' do
- described_class.new(project, user, opts).execute
+ expect(discussion.resolved?).to be(true)
+ end
- reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+ it 'added a system note to the discussion' do
+ described_class.new(project, user, opts).execute
- expect(reloaded_discussion.last_note.system).to eq(true)
- end
+ reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+ expect(reloaded_discussion.last_note.system).to eq(true)
+ end
+
+ it 'assigns the title and description for the issue' do
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.title).not_to be_nil
+ expect(issue.description).not_to be_nil
+ end
- it 'assigns the title and description for the issue' do
- issue = described_class.new(project, user, opts).execute
+ it 'can set nil explicitly to the title and description' do
+ issue = described_class.new(project, user,
+ merge_request_to_resolve_discussions_of: merge_request,
+ description: nil,
+ title: nil).execute
- expect(issue.title).not_to be_nil
- expect(issue.description).not_to be_nil
+ expect(issue.description).to be_nil
+ expect(issue.title).to be_nil
+ end
end
- it 'can set nil explicityly to the title and description' do
- issue = described_class.new(project, user,
- merge_request_for_resolving_discussions: merge_request,
- description: nil,
- title: nil).execute
+ describe 'for a merge request' do
+ let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
+
+ it 'resolves the discussion' do
+ described_class.new(project, user, opts).execute
+ discussion.first_note.reload
- expect(issue.description).to be_nil
- expect(issue.title).to be_nil
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'added a system note to the discussion' do
+ described_class.new(project, user, opts).execute
+
+ reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
+
+ expect(reloaded_discussion.last_note.system).to eq(true)
+ end
+
+ it 'assigns the title and description for the issue' do
+ issue = described_class.new(project, user, opts).execute
+
+ expect(issue.title).not_to be_nil
+ expect(issue.description).not_to be_nil
+ end
+
+ it 'can set nil explicitly to the title and description' do
+ issue = described_class.new(project, user,
+ merge_request_to_resolve_discussions_of: merge_request,
+ description: nil,
+ title: nil).execute
+
+ expect(issue.description).to be_nil
+ expect(issue.title).to be_nil
+ end
end
end
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
new file mode 100644
index 00000000000..6cc738aec08
--- /dev/null
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper.rb'
+
+class DummyService < Issues::BaseService
+ include ::Issues::ResolveDiscussions
+
+ def initialize(*args)
+ super
+ filter_resolve_discussion_params
+ end
+end
+
+describe DummyService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe "for resolving discussions" do
+ let(:discussion) { Discussion.new([create(:diff_note_on_merge_request, project: project, note: "Almost done")]) }
+ let(:merge_request) { discussion.noteable }
+ let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
+
+ describe "#merge_request_for_resolving_discussion" do
+ let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
+
+ it "finds the merge request" do
+ expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
+ end
+
+ it "only queries for the merge request once" do
+ fake_finder = double
+ fake_results = double
+
+ expect(fake_finder).to receive(:execute).and_return(fake_results).exactly(1)
+ expect(fake_results).to receive(:find_by).exactly(1)
+ expect(MergeRequestsFinder).to receive(:new).and_return(fake_finder).exactly(1)
+
+ 2.times { service.merge_request_to_resolve_discussions_of }
+ end
+ end
+
+ describe "#discussions_to_resolve" do
+ it "contains a single discussion when matching merge request and discussion are passed" do
+ service = described_class.new(
+ project,
+ user,
+ discussion_to_resolve: discussion.id,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id)
+ end
+
+ it "contains all discussions when only a merge request is passed" do
+ second_discussion = Discussion.new([create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: merge_request.target_project,
+ line_number: 15)])
+ service = described_class.new(
+ project,
+ user,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id, second_discussion.id)
+ end
+
+ it "contains only unresolved discussions" do
+ _second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved,
+ noteable: merge_request,
+ project: merge_request.target_project,
+ line_number: 15,
+ )])
+ service = described_class.new(
+ project,
+ user,
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ )
+ # We need to compare discussion id's because the Discussion-objects are rebuilt
+ # which causes the object-id's not to be different.
+ discussion_ids = service.discussions_to_resolve.map(&:id)
+
+ expect(discussion_ids).to contain_exactly(discussion.id)
+ end
+
+ it "is empty when a discussion and another merge request are passed" do
+ service = described_class.new(
+ project,
+ user,
+ discussion_to_resolve: discussion.id,
+ merge_request_to_resolve_discussions_of: other_merge_request.iid
+ )
+
+ expect(service.discussions_to_resolve).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d83b09fd32c..fa472f3e2c3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -58,6 +58,22 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'sorts issues as specified by parameters' do
+ issue1 = create(:issue, project: project, assignee_id: user3.id)
+ issue2 = create(:issue, project: project, assignee_id: user3.id)
+
+ [issue, issue1, issue2].each do |issue|
+ issue.move_to_end
+ issue.save
+ end
+
+ opts[:move_between_iids] = [issue1.iid, issue2.iid]
+
+ update_issue(opts)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
+
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
before do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0768f644036..adfa75a524f 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do
let(:commits) { Commit.decorate([commit_1], project) }
it 'creates compare object with target branch as default branch' do
- expect(merge_request.can_be_created).to eq(false)
expect(merge_request.compare).to be_present
expect(merge_request.target_branch).to eq(project.default_branch)
end
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
end
context 'same source and target branch' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 08829e4be70..b7a05907208 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -130,5 +130,15 @@ describe MergeRequests::GetUrlsService do
}])
end
end
+
+ context 'when printing_merge_request_link_enabled is false' do
+ it 'returns empty array' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ result = service.execute(existing_branch_changes)
+
+ expect(result).to eq([])
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index f92978a33a3..c2f205c389d 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
let(:project) { create(:project) }
let(:mr_merge_if_green_enabled) do
- create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
+ create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
source_branch: "master", target_branch: 'feature',
source_project: project, target_project: project, state: "opened")
end
@@ -36,7 +36,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid
- expect(merge_request.merge_when_build_succeeds).to be_truthy
+ expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
expect(merge_request.merge_user).to be user
end
@@ -62,7 +62,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it 'updates the merge params' do
- expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
+ expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
service.execute(mr_merge_if_green_enabled)
expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
@@ -82,7 +82,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
sha: merge_request_head, status: 'success')
end
- it "merges all merge requests with merge when build succeeds enabled" do
+ it "merges all merge requests with merge when the pipeline succeeds enabled" do
expect(MergeWorker).to receive(:perform_async)
service.trigger(triggering_pipeline)
end
@@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.trigger(unrelated_pipeline)
end
end
+
+ context 'when the merge request is not mergeable' do
+ let(:mr_conflict) do
+ create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
+ source_branch: 'master', target_branch: 'feature-conflict',
+ source_project: project, target_project: project)
+ end
+
+ let(:conflict_pipeline) do
+ create(:ci_pipeline, project: project, ref: mr_conflict.source_branch,
+ sha: mr_conflict.diff_head_sha, status: 'success')
+ end
+
+ it 'does not merge the merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+
+ service.trigger(conflict_pipeline)
+ end
+
+ it 'creates todos for unmergeability' do
+ expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict)
+
+ service.trigger(conflict_pipeline)
+ end
+ end
end
describe "#cancel" do
@@ -118,8 +143,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.cancel(mr_merge_if_green_enabled)
end
- it "resets all the merge_when_build_succeeds params" do
- expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+ it "resets all the pipeline succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey
expect(mr_merge_if_green_enabled.merge_params).to eq({})
expect(mr_merge_if_green_enabled.merge_user).to be nil
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 983dac6efdb..92729f68e5f 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -18,7 +18,7 @@ describe MergeRequests::RefreshService, services: true do
source_branch: 'master',
target_branch: 'feature',
target_project: @project,
- merge_when_build_succeeds: true,
+ merge_when_pipeline_succeeds: true,
merge_user: @user)
@fork_merge_request = create(:merge_request,
@@ -58,16 +58,16 @@ describe MergeRequests::RefreshService, services: true do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@merge_request, 'update', @oldrev)
- end
- it { expect(@merge_request.notes).not_to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@merge_request.merge_when_build_succeeds).to be_falsey }
- it { expect(@merge_request.diff_head_sha).to eq(@newrev) }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ expect(@merge_request.notes).not_to be_empty
+ expect(@merge_request).to be_open
+ expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.diff_head_sha).to eq(@newrev)
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push to origin repo target branch' do
@@ -76,12 +76,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('merged') }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_merged
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'manual merge of source branch' do
@@ -95,13 +97,15 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@merge_request.diffs.size).to be > 0 }
- it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('merged') }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@merge_request.diffs.size).to be > 0
+ expect(@fork_merge_request).to be_merged
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push to fork repo source branch' do
@@ -117,14 +121,14 @@ describe MergeRequests::RefreshService, services: true do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@fork_merge_request, 'update', @oldrev)
- end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes.last.note).to include('added 28 commits')
+ expect(@fork_merge_request).to be_open
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
context 'closed fork merge request' do
@@ -139,12 +143,14 @@ describe MergeRequests::RefreshService, services: true do
expect(refresh_service).not_to have_received(:execute_hooks)
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_closed }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it 'updates merge request to closed state' do
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@fork_merge_request).to be_closed
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
end
@@ -155,12 +161,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it 'updates the merge request state' do
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@fork_merge_request).to be_open
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
describe 'merge request diff' do
@@ -179,12 +187,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge request state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push new branch that exists in a merge request' do
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index a0e51681725..d33535d22af 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -59,20 +59,19 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
- to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '824be604a34828eb682305f0d963056cfac87b2d'])
+ to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d))
end
end
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'new-file-in-target',
'',
message: 'Add new file in target',
- branch_name: 'conflict-start',
- update: false)
+ branch_name: 'conflict-start')
end
before do
@@ -125,8 +124,8 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
- to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '824be604a34828eb682305f0d963056cfac87b2d'])
+ to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d))
end
it 'sets the content to the content given' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 9c92a5080c6..152c6d20daa 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -102,47 +102,19 @@ describe Notes::CreateService, services: true do
expect(subject.note).to eq(params[:note])
end
end
- end
-
- describe "award emoji" do
- before do
- project.team << [user, :master]
- end
-
- it "creates an award emoji" do
- opts = {
- note: ':smile: ',
- noteable_type: 'Issue',
- noteable_id: issue.id
- }
- note = described_class.new(project, user, opts).execute
-
- expect(note).to be_valid
- expect(note.name).to eq('smile')
- end
- it "creates regular note if emoji name is invalid" do
- opts = {
- note: ':smile: moretext:',
- noteable_type: 'Issue',
- noteable_id: issue.id
- }
- note = described_class.new(project, user, opts).execute
-
- expect(note).to be_valid
- expect(note.note).to eq(opts[:note])
- end
-
- it "normalizes the emoji name" do
- opts = {
- note: ':+1:',
- noteable_type: 'Issue',
- noteable_id: issue.id
- }
-
- expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
+ describe 'note with emoji only' do
+ it 'creates regular note' do
+ opts = {
+ note: ':smile: ',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
+ note = described_class.new(project, user, opts).execute
- described_class.new(project, user, opts).execute
+ expect(note).to be_valid
+ expect(note.note).to eq(':smile:')
+ end
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 839250b7d84..f7240969588 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -146,16 +146,6 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- it "emails the note author if they've opted into notifications about their activity" do
- add_users_with_subscription(note.project, issue)
- note.author.notified_of_own_activity = true
- reset_delivered_emails!
-
- notification.new_note(note)
-
- should_email(note.author)
- end
-
it 'filters out "mentioned in" notes' do
mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author)
@@ -486,20 +476,6 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
end
- it "emails the author if they've opted into notifications about their activity" do
- issue.author.notified_of_own_activity = true
-
- notification.new_issue(issue, issue.author)
-
- should_email(issue.author)
- end
-
- it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_issue(issue, issue.author)
-
- should_not_email(issue.author)
- end
-
it "emails subscribers of the issue's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -689,19 +665,6 @@ describe NotificationService, services: true do
should_email(subscriber_to_label_2)
end
- it "emails the current user if they've opted into notifications about their activity" do
- subscriber_to_label_2.notified_of_own_activity = true
- notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
- should_email(subscriber_to_label_2)
- end
-
- it "doesn't email the current user if they haven't opted into notifications about their activity" do
- notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
- should_not_email(subscriber_to_label_2)
- end
-
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
@@ -795,7 +758,7 @@ describe NotificationService, services: true do
update_custom_notification(:reopen_issue, @u_custom_global)
end
- it 'sends email to issue assignee and issue author' do
+ it 'sends email to issue notification recipients' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -809,6 +772,7 @@ describe NotificationService, services: true do
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
@@ -818,6 +782,32 @@ describe NotificationService, services: true do
let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
end
end
+
+ describe '#issue_moved' do
+ let(:new_issue) { create(:issue) }
+
+ it 'sends email to issue notification recipients' do
+ notification.issue_moved(issue, new_issue, @u_disabled)
+
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+ end
+ end
end
describe 'Merge Requests' do
@@ -855,20 +845,6 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- it "emails the author if they've opted into notifications about their activity" do
- merge_request.author.notified_of_own_activity = true
-
- notification.new_merge_request(merge_request, merge_request.author)
-
- should_email(merge_request.author)
- end
-
- it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_merge_request(merge_request, merge_request.author)
-
- should_not_email(merge_request.author)
- end
-
it "emails subscribers of the merge request's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -1050,28 +1026,20 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- it "notifies the merger when merge_when_build_succeeds is true" do
- merge_request.merge_when_build_succeeds = true
+ it "notifies the merger when the pipeline succeeds is true" do
+ merge_request.merge_when_pipeline_succeeds = true
notification.merge_mr(merge_request, @u_watcher)
should_email(@u_watcher)
end
- it "does not notify the merger when merge_when_build_succeeds is false" do
- merge_request.merge_when_build_succeeds = false
+ it "does not notify the merger when the pipeline succeeds is false" do
+ merge_request.merge_when_pipeline_succeeds = false
notification.merge_mr(merge_request, @u_watcher)
should_not_email(@u_watcher)
end
- it "notifies the merger when merge_when_build_succeeds is false but they've opted into notifications about their activity" do
- merge_request.merge_when_build_succeeds = false
- @u_watcher.notified_of_own_activity = true
- notification.merge_mr(merge_request, @u_watcher)
-
- should_email(@u_watcher)
- end
-
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
@@ -1251,6 +1219,48 @@ describe NotificationService, services: true do
end
end
+ describe 'Pipelines' do
+ describe '#pipeline_finished' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { create(:user) }
+ let(:u_member) { create(:user) }
+ let(:u_other) { create(:user) }
+
+ let(:commit) { project.commit }
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ project: project,
+ user: current_user,
+ ref: 'refs/heads/master',
+ sha: commit.id,
+ before_sha: '00000000')
+ end
+
+ before do
+ project.add_master(current_user)
+ project.add_master(u_member)
+ reset_delivered_emails!
+ end
+
+ context 'without custom recipients' do
+ it 'notifies the pipeline user' do
+ notification.pipeline_finished(pipeline)
+
+ should_only_email(current_user, kind: :bcc)
+ end
+ end
+
+ context 'with custom recipients' do
+ it 'notifies the custom recipients' do
+ users = [u_member, u_other]
+ notification.pipeline_finished(pipeline, users.map(&:notification_email))
+
+ should_only_email(*users, kind: :bcc)
+ end
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index af515ad2e0e..62f21049b0b 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -50,7 +50,7 @@ describe Projects::CreateService, '#execute', services: true do
context 'error handling' do
it 'handles invalid options' do
- opts.merge!({ default_branch: 'master' } )
+ opts[:default_branch] = 'master'
expect(create_project(user, opts)).to eq(nil)
end
end
@@ -67,7 +67,7 @@ describe Projects::CreateService, '#execute', services: true do
context 'wiki_enabled false does not create wiki repository directory' do
it do
- opts.merge!(wiki_enabled: false)
+ opts[:wiki_enabled] = false
project = create_project(user, opts)
path = ProjectWiki.new(project, user).send(:path_to_repo)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ab6e8f537ba..e5917bb0b7a 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do
end
end
+ context 'with blocked import_URL' do
+ it 'fails with localhost' do
+ project.import_url = 'https://localhost:9000/vim/vim.git'
+
+ result = described_class.new(project, user).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to end_with 'Blocked import URL.'
+ end
+
+ it 'fails with port 25' do
+ project.import_url = "https://github.com:25/vim/vim.git"
+
+ result = described_class.new(project, user).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to end_with 'Blocked import URL.'
+ end
+ end
+
def stub_github_omniauth_provider
provider = OpenStruct.new(
'name' => 'github',
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 411b22a0fb8..f75fdd9e03f 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -26,6 +26,28 @@ describe Projects::UpdatePagesService do
build.update_attributes(artifacts_metadata: metadata)
end
+ describe 'pages artifacts' do
+ context 'with expiry date' do
+ before do
+ build.artifacts_expire_in = "2 days"
+ end
+
+ it "doesn't delete artifacts" do
+ expect(execute).to eq(:success)
+
+ expect(build.reload.artifacts_file?).to eq(true)
+ end
+ end
+
+ context 'without expiry date' do
+ it "does delete artifacts" do
+ expect(execute).to eq(:success)
+
+ expect(build.reload.artifacts_file?).to eq(false)
+ end
+ end
+ end
+
it 'succeeds' do
expect(project.pages_deployed?).to be_falsey
expect(execute).to eq(:success)
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index c42eeba4b9c..150c8ccaef7 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -10,7 +10,7 @@ describe Projects::UploadService, services: true do
context 'for valid gif file' do
before do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
- @link_to_file = upload_file(@project.repository, gif)
+ @link_to_file = upload_file(@project, gif)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -23,7 +23,7 @@ describe Projects::UploadService, services: true do
before do
png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
'image/png')
- @link_to_file = upload_file(@project.repository, png)
+ @link_to_file = upload_file(@project, png)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -35,7 +35,7 @@ describe Projects::UploadService, services: true do
context 'for valid jpg file' do
before do
jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
- @link_to_file = upload_file(@project.repository, jpg)
+ @link_to_file = upload_file(@project, jpg)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -47,7 +47,7 @@ describe Projects::UploadService, services: true do
context 'for txt file' do
before do
txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
- @link_to_file = upload_file(@project.repository, txt)
+ @link_to_file = upload_file(@project, txt)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -60,14 +60,14 @@ describe Projects::UploadService, services: true do
before do
txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
allow(txt).to receive(:size) { 1000.megabytes.to_i }
- @link_to_file = upload_file(@project.repository, txt)
+ @link_to_file = upload_file(@project, txt)
end
it { expect(@link_to_file).to eq(nil) }
end
end
- def upload_file(repository, file)
- Projects::UploadService.new(repository, file).execute
+ def upload_file(project, file)
+ Projects::UploadService.new(project, file).execute
end
end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 7d4eff3b6ef..6ea8f309981 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -6,8 +6,8 @@ describe ProtectedBranches::CreateService, services: true do
let(:params) do
{
name: 'master',
- merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
- push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }],
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
}
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 0b0925983eb..52e8678cb9d 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -267,6 +267,14 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'award command' do
+ it 'toggle award 100 emoji if content containts /award :100:' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(emoji_award: "100")
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -654,6 +662,37 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ context '/award command' do
+ it_behaves_like 'award command' do
+ let(:content) { '/award :100:' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'award command' do
+ let(:content) { '/award :100:' }
+ let(:issuable) { merge_request }
+ end
+
+ context 'ignores command with no argument' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/award' }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'ignores non-existing / invalid emojis' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/award noop' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/award :lorem_ipsum:' }
+ let(:issuable) { issue }
+ end
+ end
+ end
+
context '/target_branch command' do
let(:non_empty_project) { create(:project) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index db9f1231682..11037a4917b 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -5,6 +5,7 @@ describe SystemHooksService, services: true do
let(:project) { create :project }
let(:project_member) { create :project_member }
let(:key) { create(:key, user: user) }
+ let(:deploy_key) { create(:key) }
let(:group) { create(:group) }
let(:group_member) { create(:group_member) }
@@ -18,6 +19,8 @@ describe SystemHooksService, services: true do
it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
+ it { expect(event_data(deploy_key, :create)).to include(:key, :id) }
+ it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) }
it do
project.old_path_with_namespace = 'renamed_from_path'
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 7f027ae02a2..36a17a3bf2e 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -215,13 +215,13 @@ describe SystemNoteService, services: true do
end
end
- describe '.merge_when_build_succeeds' do
+ describe '.merge_when_pipeline_succeeds' do
let(:pipeline) { build(:ci_pipeline_without_jobs )}
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
- subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) }
+ subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) }
it_behaves_like 'a system note'
@@ -230,12 +230,12 @@ describe SystemNoteService, services: true do
end
end
- describe '.cancel_merge_when_build_succeeds' do
+ describe '.cancel_merge_when_pipeline_succeeds' do
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
- subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
+ subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) }
it_behaves_like 'a system note'
@@ -418,45 +418,6 @@ describe SystemNoteService, services: true do
to be_truthy
end
end
-
- context 'when noteable is an Issue' do
- let(:issue) { create(:issue, project: project) }
-
- it 'is truthy when issue is closed' do
- issue.close
-
- expect(described_class.cross_reference_disallowed?(issue, project.commit)).
- to be_truthy
- end
-
- it 'is falsey when issue is open' do
- expect(described_class.cross_reference_disallowed?(issue, project.commit)).
- to be_falsy
- end
- end
-
- context 'when noteable is a Merge Request' do
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
- it 'is truthy when merge request is closed' do
- allow(merge_request).to receive(:closed?).and_return(:true)
-
- expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
- to be_truthy
- end
-
- it 'is truthy when merge request is merged' do
- allow(merge_request).to receive(:closed?).and_return(:true)
-
- expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
- to be_truthy
- end
-
- it 'is falsey when merge request is open' do
- expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
- to be_falsy
- end
- end
end
describe '.cross_reference_exists?' do
@@ -631,7 +592,7 @@ describe SystemNoteService, services: true do
jira_service_settings
end
- noteable_types = ["merge_requests", "commit"]
+ noteable_types = %w(merge_requests commit)
noteable_types.each do |type|
context "when noteable is a #{type}" do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 9f24cc0f3f2..3645b73b039 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -298,6 +298,10 @@ describe TodoService, services: true do
expect(second_todo.reload.state?(new_state)).to be true
end
+ it 'returns the updated ids' do
+ expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id])
+ end
+
describe 'cached counts' do
it 'updates when todos change' do
expect(john_doe.todos.where(state: new_state).count).to eq(0)
@@ -680,7 +684,7 @@ describe TodoService, services: true do
end
it 'creates a pending todo for merge_user' do
- mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+ mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
service.merge_request_build_failed(mr_unassigned)
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
@@ -700,13 +704,13 @@ describe TodoService, services: true do
describe '#merge_request_became_unmergeable' do
it 'creates a pending todo for a merge_user' do
- mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+ mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
service.merge_request_became_unmergeable(mr_unassigned)
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
end
end
-
+
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
@@ -752,7 +756,7 @@ describe TodoService, services: true do
issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
expect(john_doe.todos_pending_count).to eq(0)
- expect(john_doe).to receive(:update_todos_count_cache)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.new_issue(issue, author)
@@ -779,29 +783,27 @@ describe TodoService, services: true do
.to change { todo.reload.state }.from('pending').to('done')
end
- it 'returns the number of updated todos' do # Needed on API
+ it 'returns the ids of updated todos' do # Needed on API
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
- expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+ expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id])
end
context 'when some of the todos are done already' do
- before do
- create(:todo, :mentioned, user: john_doe, target: issue, project: project)
- create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
- end
+ let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
+ let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
- it 'returns the number of those still pending' do
+ it 'returns the ids of those still pending' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
- expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
end
- it 'returns 0 if all are done' do
+ it 'returns an empty array if all are done' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
- expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
end
end
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
index c0bf27c698c..922e82445d0 100644
--- a/spec/services/users/destroy_spec.rb
+++ b/spec/services/users/destroy_spec.rb
@@ -24,6 +24,54 @@ describe Users::DestroyService, services: true do
end
end
+ context "a deleted user's issues" do
+ let(:project) { create :project }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "for an issue the user has created" do
+ let!(:issue) { create(:issue, project: project, author: user) }
+
+ before do
+ service.execute(user)
+ end
+
+ it 'does not delete the issue' do
+ expect(Issue.find_by_id(issue.id)).to be_present
+ end
+
+ it 'migrates the issue so that the "Ghost User" is the issue owner' do
+ migrated_issue = Issue.find_by_id(issue.id)
+
+ expect(migrated_issue.author).to eq(User.ghost)
+ end
+
+ it 'blocks the user before migrating issues to the "Ghost User' do
+ expect(user).to be_blocked
+ end
+ end
+
+ context "for an issue the user was assigned to" do
+ let!(:issue) { create(:issue, project: project, assignee: user) }
+
+ before do
+ service.execute(user)
+ end
+
+ it 'does not delete issues the user is assigned to' do
+ expect(Issue.find_by_id(issue.id)).to be_present
+ end
+
+ it 'migrates the issue so that it is "Unassigned"' do
+ migrated_issue = Issue.find_by_id(issue.id)
+
+ expect(migrated_issue.assignee).to be_nil
+ end
+ end
+ end
+
context "solo owned groups present" do
let(:solo_owned) { create(:group) }
let(:member) { create(:group_member) }
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 690fe979492..08733d6dcf1 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do
it 'sets the values to the access levels' do
expect(hash.values).to eq([Gitlab::Access::MASTER])
end
+
+ context 'personal projects' do
+ it 'includes the project with the right access level' do
+ expect(hash[project.id]).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'projects the user is a member of' do
+ let!(:other_project) { create(:empty_project) }
+
+ before do
+ other_project.team.add_reporter(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER)
+ end
+ end
+
+ context 'projects of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let!(:other_project) { create(:project, group: group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER)
+ end
+ end
+
+ context 'projects of subgroups of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let!(:other_project) { create(:project, group: nested_group) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'projects shared with groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:other_project) { create(:empty_project) }
+ let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST)
+ end
+ end
+
+ context 'projects shared with subgroups of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:other_project) { create(:empty_project) }
+ let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
end
describe '#current_authorizations_per_project' do
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index b507d38f472..ac2c89b3ff9 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -15,9 +15,9 @@ module SimpleCovEnv
def configure_job
SimpleCov.configure do
- if ENV['CI_BUILD_NAME']
- coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}"
- command_name ENV['CI_BUILD_NAME']
+ if ENV['CI_JOB_NAME']
+ coverage_dir "coverage/#{ENV['CI_JOB_NAME']}"
+ command_name ENV['CI_JOB_NAME']
end
if ENV['CI']
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5fda7c63cdb..ceb3209331f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -43,14 +43,27 @@ RSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
+ config.include ApiHelpers, :api
config.infer_spec_type_from_file_location!
+
+ config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata|
+ metadata[:api] = true
+ end
+
config.raise_errors_for_deprecations!
config.before(:suite) do
TestEnv.init
end
+ if ENV['CI']
+ # Retry only on feature specs that use JS
+ config.around :each, :js do |ex|
+ ex.run_with_retry retry: 3
+ end
+ end
+
config.around(:each, :caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
diff --git a/spec/support/api/issues_resolving_discussions_shared_examples.rb b/spec/support/api/issues_resolving_discussions_shared_examples.rb
new file mode 100644
index 00000000000..d26d279363c
--- /dev/null
+++ b/spec/support/api/issues_resolving_discussions_shared_examples.rb
@@ -0,0 +1,15 @@
+shared_examples 'creating an issue resolving discussions through the API' do
+ it 'creates a new project issue' do
+ expect(response).to have_http_status(:created)
+ end
+
+ it 'resolves the discussions in a merge request' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'assigns a description to the issue mentioning the merge request' do
+ expect(json_response['description']).to include(merge_request.to_reference)
+ end
+end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index 210cd5817e0..16a3cf06be7 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -7,13 +7,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
context 'with an unauthorized user' do
- subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), duration: '1w') }
it_behaves_like 'an unauthorized API user'
end
it "sets the time estimate for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
expect(response).to have_http_status(200)
expect(json_response['human_time_estimate']).to eq('1w')
@@ -21,12 +21,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe 'updating the current estimate' do
before do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
end
context 'when duration has a bad format' do
it 'does not modify the original estimate' do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: 'foo'
expect(response).to have_http_status(400)
expect(issuable.reload.human_time_estimate).to eq('1w')
@@ -35,7 +35,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'with a valid duration' do
it 'updates the estimate' do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '3w1h'
expect(response).to have_http_status(200)
expect(issuable.reload.human_time_estimate).to eq('3w 1h')
@@ -46,13 +46,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
context 'with an unauthorized user' do
- subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets the time estimate for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user)
expect(response).to have_http_status(200)
expect(json_response['time_estimate']).to eq(0)
@@ -62,7 +62,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
context 'with an unauthorized user' do
subject do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", non_member),
duration: '2h'
end
@@ -70,7 +70,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "add spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '2h'
expect(response).to have_http_status(201)
@@ -81,7 +81,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'subtracts time of the total spent time' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
expect(response).to have_http_status(201)
@@ -93,7 +93,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'does not modify the total time spent' do
issuable.update_attributes!(spend_time: { duration: 7200, user: user })
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1w'
expect(response).to have_http_status(400)
@@ -104,13 +104,13 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
context 'with an unauthorized user' do
- subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) }
it_behaves_like 'an unauthorized API user'
end
it "resets spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
@@ -122,7 +122,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
issuable.update_attributes!(spend_time: { duration: 1800, user: user },
time_estimate: 3600)
- get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+ get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
expect(response).to have_http_status(200)
expect(json_response['total_time_spent']).to eq(1800)
diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..f982b10d999
--- /dev/null
+++ b/spec/support/api/v3/time_tracking_shared_examples.rb
@@ -0,0 +1,128 @@
+shared_examples 'V3 time tracking endpoints' do |issuable_name|
+ issuable_collection_name = issuable_name.pluralize
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "sets the time estimate for #{issuable_name}" do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['human_time_estimate']).to eq('1w')
+ end
+
+ describe 'updating the current estimate' do
+ before do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ end
+
+ context 'when duration has a bad format' do
+ it 'does not modify the original estimate' do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(issuable.reload.human_time_estimate).to eq('1w')
+ end
+ end
+
+ context 'with a valid duration' do
+ it 'updates the estimate' do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+
+ expect(response).to have_http_status(200)
+ expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets the time estimate for #{issuable_name}" do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['time_estimate']).to eq(0)
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
+ context 'with an unauthorized user' do
+ subject do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+ duration: '2h'
+ end
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "add spent time for #{issuable_name}" do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '2h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['human_total_time_spent']).to eq('2h')
+ end
+
+ context 'when subtracting time' do
+ it 'subtracts time of the total spent time' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['total_time_spent']).to eq(3600)
+ end
+ end
+
+ context 'when time to subtract is greater than the total spent time' do
+ it 'does not modify the total time spent' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1w'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
+ context 'with an unauthorized user' do
+ subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets spent time for #{issuable_name}" do
+ post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(0)
+ end
+ end
+
+ describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
+ it "returns the time stats for #{issuable_name}" do
+ issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ time_estimate: 3600)
+
+ get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(1800)
+ expect(json_response['time_estimate']).to eq(3600)
+ end
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index ae6e708cf87..35d1e1cfc7d 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -49,8 +49,4 @@ module ApiHelpers
''
end
end
-
- def json_response
- @_json_response ||= JSON.parse(response.body)
- end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 16d5f2bf0b8..aa14709bc9c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,9 +1,10 @@
require 'capybara/rails'
require 'capybara/rspec'
require 'capybara/poltergeist'
+require 'capybara-screenshot/rspec'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
@@ -21,12 +22,8 @@ end
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
-unless ENV['CI'] || ENV['CI_SERVER']
- require 'capybara-screenshot/rspec'
-
- # Keep only the screenshots generated from the last failing test suite
- Capybara::Screenshot.prune_strategy = :keep_last_run
-end
+# Keep only the screenshots generated from the last failing test suite
+Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
config.before(:suite) do
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
index 72af2c70324..b4b016e408f 100644
--- a/spec/support/carrierwave.rb
+++ b/spec/support/carrierwave.rb
@@ -1,7 +1,7 @@
-CarrierWave.root = 'tmp/tests/uploads'
+CarrierWave.root = File.expand_path('tmp/tests/public', Rails.root)
RSpec.configure do |config|
config.after(:each) do
- FileUtils.rm_rf('tmp/tests/uploads')
+ FileUtils.rm_rf(CarrierWave.root)
end
end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 6ed55289ed9..c864a705ca4 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -9,14 +9,7 @@ module CycleAnalyticsHelpers
commit_shas = Array.new(count) do |index|
filename = random_git_name
- options = {
- committer: project.repository.user_to_committer(user),
- author: project.repository.user_to_committer(user),
- commit: { message: message, branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
-
- commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name)
project.repository.commit(commit_sha)
commit_sha
@@ -35,13 +28,12 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.commit_file(
+ sha = project.repository.create_file(
user,
random_git_name,
'content',
message: 'commit message',
- branch_name: source_branch,
- update: false)
+ branch_name: source_branch)
project.repository.commit(sha)
opts = {
diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb
new file mode 100644
index 00000000000..984ec7d2741
--- /dev/null
+++ b/spec/support/dropzone_helper.rb
@@ -0,0 +1,37 @@
+module DropzoneHelper
+ # Provides a way to perform `attach_file` for a Dropzone-based file input
+ #
+ # This is accomplished by creating a standard HTML file input on the page,
+ # performing `attach_file` on that field, and then triggering the appropriate
+ # Dropzone events to perform the actual upload.
+ #
+ # This method waits for the upload to complete before returning.
+ def dropzone_file(file_path)
+ # Generate a fake file input that Capybara can attach to
+ page.execute_script <<-JS.strip_heredoc
+ var fakeFileInput = window.$('<input/>').attr(
+ {id: 'fakeFileInput', type: 'file'}
+ ).appendTo('body');
+
+ window._dropzoneComplete = false;
+ JS
+
+ # Attach the file to the fake input selector with Capybara
+ attach_file('fakeFileInput', file_path)
+
+ # Manually trigger a Dropzone "drop" event with the fake input's file list
+ page.execute_script <<-JS.strip_heredoc
+ var fileList = [$('#fakeFileInput')[0].files[0]];
+ var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+
+ var dropzone = $('.div-dropzone')[0].dropzone;
+ dropzone.on('queuecomplete', function() {
+ window._dropzoneComplete = true;
+ });
+ dropzone.listeners[0].events.drop(e);
+ JS
+
+ # Wait until Dropzone's fired `queuecomplete`
+ loop until page.evaluate_script('window._dropzoneComplete === true')
+ end
+end
diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
new file mode 100644
index 00000000000..4a946995f84
--- /dev/null
+++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb
@@ -0,0 +1,41 @@
+shared_examples 'creating an issue for a discussion' do
+ it 'shows an issue with the title filled in' do
+ title_field = page.find_field('issue[title]')
+
+ expect(title_field.value).to include(merge_request.title)
+ end
+
+ it 'has a mention of the discussion in the description' do
+ description_field = page.find_field('issue[description]')
+
+ expect(description_field.value).to include(discussion.first_note.note)
+ end
+
+ it 'can create a new issue for the project' do
+ expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1)
+ end
+
+ it 'resolves the discussion in the merge request' do
+ click_button 'Submit issue'
+
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to eq(true)
+ end
+
+ it 'shows a flash messaage after resolving a discussion' do
+ click_button 'Submit issue'
+
+ page.within '.flash-notice' do
+ # Only check for the word 'Resolved' since the spec might have resolved
+ # multiple discussions
+ expect(page).to have_content('Resolved')
+ end
+ end
+
+ it 'has a hidden field for the merge request' do
+ merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false)
+
+ expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+ end
+end
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
new file mode 100644
index 00000000000..9a3b0a731ad
--- /dev/null
+++ b/spec/support/features/rss_shared_examples.rb
@@ -0,0 +1,23 @@
+shared_examples "an autodiscoverable RSS feed with current_user's private token" do
+ it "has an RSS autodiscovery link tag with current_user's private token" do
+ expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false)
+ end
+end
+
+shared_examples "it has an RSS button with current_user's private token" do
+ it "shows the RSS button with current_user's private token" do
+ expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']")
+ end
+end
+
+shared_examples "an autodiscoverable RSS feed without a private token" do
+ it "has an RSS autodiscovery link tag without a private token" do
+ expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false)
+ end
+end
+
+shared_examples "it has an RSS button without a private token" do
+ it "shows the RSS button without a private token" do
+ expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])")
+ end
+end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index 58f6636e680..6b009b132b6 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -3,16 +3,25 @@ module FilteredSearchHelpers
page.find('.filtered-search')
end
- def input_filtered_search(search_term, submit: true)
- filtered_search.set(search_term)
+ # Enables input to be set (similar to copy and paste)
+ def input_filtered_search(search_term, submit: true, extra_space: true)
+ search = search_term
+ if extra_space
+ # Add an extra space to engage visual tokens
+ search = "#{search_term} "
+ end
+
+ filtered_search.set(search)
if submit
filtered_search.send_keys(:enter)
end
end
+ # Enables input to be added character by character
def input_filtered_search_keys(search_term)
- filtered_search.send_keys(search_term)
+ # Add an extra space to engage visual tokens
+ filtered_search.send_keys("#{search_term} ")
filtered_search.send_keys(:enter)
end
@@ -34,4 +43,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end
+
+ def expect_filtered_search_input_empty
+ expect(find('.filtered-search').value).to eq('')
+ end
+
+ # Iterates through each visual token inside
+ # .tokens-container to make sure the correct names and values are rendered
+ def expect_tokens(tokens)
+ page.find '.filtered-search-input-container .tokens-container' do
+ page.all(:css, '.tokens-container li').each_with_index do |el, index|
+ token_name = tokens[index][:name]
+ token_value = tokens[index][:value]
+
+ expect(el.find('.name')).to have_content(token_name)
+ if token_value
+ expect(el.find('.value')).to have_content(token_value)
+ end
+ end
+ end
+ end
+
+ def default_placeholder
+ 'Search or filter results...'
+ end
+
+ def get_filtered_search_placeholder
+ find('.filtered-search')['placeholder']
+ end
end
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
index dac94dfc31e..4c0f556e736 100644
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -3,11 +3,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
@issuable_ids = []
2.times do
- if issuable_type == :issue
- issuable = create(issuable_type, project: project)
- else
- issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
- end
+ issuable =
+ if issuable_type == :issue
+ create(issuable_type, project: project)
+ else
+ create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
@issuable_ids << issuable.id
@@ -21,7 +22,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
if action
get action
else
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
end
meta_data = assigns(:issuable_meta_data)
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 0b8729db0f9..a982b159b48 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -5,7 +5,7 @@ require 'gitlab/popen'
module JavaScriptFixturesHelpers
include Gitlab::Popen
- FIXTURE_PATH = 'spec/javascripts/fixtures'
+ FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
# Public: Removes all fixture files from given directory
#
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 929fc0c5182..97ae0b6afc5 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,5 +1,5 @@
module JiraServiceHelper
- JIRA_URL = "http://jira.example.net"
+ JIRA_URL = "http://jira.example.net".freeze
JIRA_API = JIRA_URL + "/rest/api/2"
def jira_service_settings
diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb
new file mode 100644
index 00000000000..e8d2ef2d7f0
--- /dev/null
+++ b/spec/support/json_response_helpers.rb
@@ -0,0 +1,9 @@
+shared_context 'JSON response' do
+ let(:json_response) { JSON.parse(response.body) }
+end
+
+RSpec.configure do |config|
+ config.include_context 'JSON response', type: :controller
+ config.include_context 'JSON response', type: :request
+ config.include_context 'JSON response', :api
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index 444612cf871..b5ed71ba3be 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -2,23 +2,24 @@ module KubernetesHelpers
include Gitlab::Kubernetes
def kube_discovery_body
- { "kind" => "APIResourceList",
+ {
+ "kind" => "APIResourceList",
"resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
- ],
+ ]
}
end
def kube_pods_body(*pods)
{ "kind" => "PodList",
- "items" => [ kube_pod ],
- }
+ "items" => [kube_pod] }
end
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_pod(app: "valid-pod-label")
- { "metadata" => {
+ {
+ "metadata" => {
"name" => "kube-pod",
"creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => { "app" => app },
@@ -29,7 +30,7 @@ module KubernetesHelpers
{ "name" => "container-1" },
],
},
- "status" => { "phase" => "Running" },
+ "status" => { "phase" => "Running" }
}
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ad1eed5b369..9ffb00be0b8 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -15,11 +15,12 @@ module LoginHelpers
# user = create(:user)
# login_as(user)
def login_as(user_or_role)
- if user_or_role.kind_of?(User)
- @user = user_or_role
- else
- @user = create(user_or_role)
- end
+ @user =
+ if user_or_role.is_a?(User)
+ user_or_role
+ else
+ create(user_or_role)
+ end
login_with(@user)
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index a79386b5db9..dea0015f105 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -79,8 +79,8 @@ class MarkdownFeature
def xproject
@xproject ||= begin
- namespace = create(:namespace, name: 'cross-reference')
- create(:project, namespace: namespace) do |project|
+ group = create(:group, :nested)
+ create(:project, namespace: group) do |project|
project.team << [user, :developer]
end
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index ceddb656596..7d238850520 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -38,7 +38,7 @@ module AccessMatchers
end
def description_for(user, type)
- if user.kind_of?(User)
+ if user.is_a?(User)
# User#inspect displays too much information for RSpec's descriptions
"be #{type} for the specified user"
else
diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb
new file mode 100644
index 00000000000..d9d59ec12ec
--- /dev/null
+++ b/spec/support/matchers/email_matchers.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_html_escaped_body_text do |expected|
+ match do |actual|
+ expect(actual).to have_body_text(ERB::Util.html_escape(expected))
+ end
+end
diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb
new file mode 100644
index 00000000000..d7a53820684
--- /dev/null
+++ b/spec/support/matchers/gitaly_matchers.rb
@@ -0,0 +1,3 @@
+RSpec::Matchers.define :post_receive_request_with_repo_path do |path|
+ match { |actual| actual.repository.path == path }
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 97b8b342eb2..bbbbaf4c5e8 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -26,10 +26,11 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('img.emoji', count: 10)
+ expect(actual).to have_selector('gl-emoji', count: 10)
- image = actual.at_css('img.emoji')
- expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets')
+ emoji_element = actual.at_css('gl-emoji')
+ expect(emoji_element['data-name'].to_s).not_to be_empty
+ expect(emoji_element['data-unicode-version'].to_s).not_to be_empty
end
end
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb
index 4621d17549b..f8b7d0527ba 100644
--- a/spec/support/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass|
before do
_ = issuable
- login_as(user)
+ login_as(user) if user
visit path
end
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
new file mode 100644
index 00000000000..a52d8f37d14
--- /dev/null
+++ b/spec/support/prometheus_helpers.rb
@@ -0,0 +1,117 @@
+module PrometheusHelpers
+ def prometheus_memory_query(environment_slug)
+ %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
+ end
+
+ def prometheus_cpu_query(environment_slug)
+ %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+ end
+
+ def prometheus_query_url(prometheus_query)
+ query = { query: prometheus_query }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago)
+ query = {
+ query: prometheus_query,
+ start: start.to_f,
+ end: Time.now.utc.to_f,
+ step: 1.minute.to_i
+ }.to_query
+
+ "https://prometheus.example.com/api/v1/query_range?#{query}"
+ end
+
+ def stub_prometheus_request(url, body: {}, status: 200)
+ WebMock.stub_request(:get, url)
+ .to_return({
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body.to_json
+ })
+ end
+
+ def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
+ stub_prometheus_request(
+ prometheus_query_url(prometheus_memory_query(environment_slug)),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_memory_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ stub_prometheus_request(
+ prometheus_query_url(prometheus_cpu_query(environment_slug)),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_cpu_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ end
+
+ def prometheus_data(last_update: Time.now.utc)
+ {
+ success: true,
+ metrics: {
+ memory_values: prometheus_values_body('matrix').dig(:data, :result),
+ memory_current: prometheus_value_body('vector').dig(:data, :result),
+ cpu_values: prometheus_values_body('matrix').dig(:data, :result),
+ cpu_current: prometheus_value_body('vector').dig(:data, :result)
+ },
+ last_update: last_update
+ }
+ end
+
+ def prometheus_empty_body(type)
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": []
+ }
+ }
+ end
+
+ def prometheus_value_body(type = 'vector')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "value": [
+ 1488772511.004,
+ "0.000041021495238095323"
+ ]
+ }
+ ]
+ }
+ }
+ end
+
+ def prometheus_values_body(type = 'matrix')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "values": [
+ [1488758662.506, "0.00002996364761904785"],
+ [1488758722.506, "0.00003090239047619091"]
+ ]
+ }
+ ]
+ }
+ }
+ end
+end
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index 73f375c481b..e9d5c7b12ae 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
eos
)
end
-
+
def another_sample_commit
OpenStruct.new(
id: "e56497bb5f03a90a51293fc6d516788730953899",
@@ -100,13 +100,13 @@ eos
}
]
- commits = [
- '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
- '570e7b2abdd848b95f2f578043fc23bd6f6fd24d',
- '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
- 'd14d6c0abdd253381df51a723d58691b2ee1ab08',
- 'c1acaa58bbcbc3eafe538cb8274ba387047b69f8',
- ].reverse # last commit is recent one
+ commits = %w(
+ 5937ac0a7beb003549fc5fd26fc247adbce4a52e
+ 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
+ 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ d14d6c0abdd253381df51a723d58691b2ee1ab08
+ c1acaa58bbcbc3eafe538cb8274ba387047b69f8
+ ).reverse # last commit is recent one
OpenStruct.new(
source_branch: 'master',
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 03fa0a66b9a..f55fee28ff9 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
module SeedHelper
- GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
+ GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze
def ensure_seeds
if File.exist?(SEED_REPOSITORY_PATH)
@@ -25,7 +25,7 @@ module SeedHelper
end
def create_bare_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
chdir: SEED_REPOSITORY_PATH,
out: '/dev/null',
err: '/dev/null')
@@ -45,7 +45,7 @@ module SeedHelper
system(git_env, *%w(git branch -t feature origin/feature),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
- system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
end
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
index 9f2cd7c67c5..99a500bbbb1 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/seed_repo.rb
@@ -25,64 +25,64 @@
module SeedRepo
module BigCommit
- ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
- PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
- MESSAGE = "Files, encoding and much more"
- AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze
+ PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze
+ MESSAGE = "Files, encoding and much more".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
FILES_COUNT = 2
end
module Commit
- ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
- PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
- AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
- FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
+ ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze
+ PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze
+ MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze
FILES_COUNT = 2
- C_FILE_PATH = "files/ruby"
- C_FILES = ["popen.rb", "regex.rb", "version_info.rb"]
- BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
- BLOB_FILE_PATH = "app/views/keys/show.html.haml"
+ C_FILE_PATH = "files/ruby".freeze
+ C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
+ BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}.freeze
+ BLOB_FILE_PATH = "app/views/keys/show.html.haml".freeze
end
module EmptyCommit
- ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
- PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
- MESSAGE = "Empty commit"
- AUTHOR_FULL_NAME = "Rémy Coutable"
- FILES = []
+ ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze
+ PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ MESSAGE = "Empty commit".freeze
+ AUTHOR_FULL_NAME = "Rémy Coutable".freeze
+ FILES = [].freeze
FILES_COUNT = FILES.count
end
module EncodingCommit
- ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
- PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8"
- MESSAGE = "Add ISO-8859-encoded file"
- AUTHOR_FULL_NAME = "Stan Hu"
- FILES = ["encoding/iso8859.txt"]
+ ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8".freeze
+ MESSAGE = "Add ISO-8859-encoded file".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["encoding/iso8859.txt"].freeze
FILES_COUNT = FILES.count
end
module FirstCommit
- ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
+ ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze
PARENT_ID = nil
- MESSAGE = "Initial commit"
- AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
- FILES = ["LICENSE", ".gitignore", "README.md"]
+ MESSAGE = "Initial commit".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["LICENSE", ".gitignore", "README.md"].freeze
FILES_COUNT = 3
end
module LastCommit
- ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
- PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
- MESSAGE = "Merge branch 'master' into 'master'"
- AUTHOR_FULL_NAME = "Stan Hu"
- FILES = ["bin/executable"]
+ ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6".freeze
+ PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9".freeze
+ MESSAGE = "Merge branch 'master' into 'master'".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["bin/executable"].freeze
FILES_COUNT = FILES.count
end
module Repo
- HEAD = "master"
+ HEAD = "master".freeze
BRANCHES = %w[
feature
fix
@@ -93,14 +93,14 @@ module SeedRepo
gitattributes-updated
master
merge-test
- ]
- TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
+ ].freeze
+ TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1].freeze
end
module RubyBlob
- ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
- NAME = "popen.rb"
- CONTENT = <<-eos
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
+ NAME = "popen.rb".freeze
+ CONTENT = <<-eos.freeze
require 'fileutils'
require 'open3'
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index d30cc8ff9f2..0d526045012 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -12,7 +12,7 @@
module Select2Helper
def select2(value, options = {})
- raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
selector = options.fetch(:from)
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index c64574679b6..81d06dc2a3d 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -11,7 +11,7 @@ shared_examples 'new issuable record that supports slash commands' do
let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
let(:issuable) { described_class.new(project, user, params).execute }
- before { project.team << [assignee, :master ] }
+ before { project.team << [assignee, :master] }
context 'with labels in command only' do
let(:example_params) do
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 93f96cacc00..a01ef576234 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -35,7 +35,7 @@ module StubGitlabCalls
{ "tags" => tags }
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
- JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
+ JSON.parse(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index b87232a350b..f1d226b6ae3 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -38,7 +38,7 @@ module TestEnv
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
'csv' => '3dd0896'
- }
+ }.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
# need to keep all the branches in sync.
@@ -48,7 +48,7 @@ module TestEnv
'master' => '5937ac0',
'remove-submodule' => '2a33e0c',
'conflict-resolvable-fork' => '404fa3f'
- }
+ }.freeze
# Test environment
#
@@ -135,7 +135,7 @@ module TestEnv
def copy_repo(project)
base_repo_path = File.expand_path(factory_repo_path_bare)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
@@ -143,7 +143,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages.default
+ Gitlab.config.repositories.storages.default['path']
end
def backup_path
@@ -152,7 +152,7 @@ module TestEnv
def copy_forked_repo_with_submodules(project)
base_repo_path = File.expand_path(forked_repo_path_bare)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
new file mode 100644
index 00000000000..7cf5a65eeed
--- /dev/null
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -0,0 +1,62 @@
+shared_context 'unique ips sign in limit' do
+ include StubENV
+ before(:each) do
+ Gitlab::Redis.with(&:flushall)
+ end
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+ current_application_settings.update!(
+ unique_ips_limit_enabled: true,
+ unique_ips_limit_time_window: 10000
+ )
+ end
+
+ def change_ip(ip)
+ allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip)
+ end
+
+ def request_from_ip(ip)
+ change_ip(ip)
+ request
+ response
+ end
+
+ def operation_from_ip(ip)
+ change_ip(ip)
+ operation
+ end
+end
+
+shared_examples 'user login operation with unique ip limit' do
+ include_context 'unique ips sign in limit' do
+ before { current_application_settings.update!(unique_ips_limit_per_user: 1) }
+
+ it 'allows user authenticating from the same ip' do
+ expect { operation_from_ip('ip') }.not_to raise_error
+ expect { operation_from_ip('ip') }.not_to raise_error
+ end
+
+ it 'blocks user authenticating from two distinct ips' do
+ expect { operation_from_ip('ip') }.not_to raise_error
+ expect { operation_from_ip('ip2') }.to raise_error(Gitlab::Auth::TooManyIps)
+ end
+ end
+end
+
+shared_examples 'user login request with unique ip limit' do |success_status = 200|
+ include_context 'unique ips sign in limit' do
+ before { current_application_settings.update!(unique_ips_limit_per_user: 1) }
+
+ it 'allows user authenticating from the same ip' do
+ expect(request_from_ip('ip')).to have_http_status(success_status)
+ expect(request_from_ip('ip')).to have_http_status(success_status)
+ end
+
+ it 'blocks user authenticating from two distinct ips' do
+ expect(request_from_ip('ip')).to have_http_status(success_status)
+ expect(request_from_ip('ip2')).to have_http_status(403)
+ end
+ end
+end
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
new file mode 100644
index 00000000000..365c34448ac
--- /dev/null
+++ b/spec/support/update_invalid_issuable.rb
@@ -0,0 +1,57 @@
+shared_examples 'update invalid issuable' do |klass|
+ let(:params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issuable.iid
+ }
+ end
+
+ let(:issuable) do
+ klass == Issue ? issue : merge_request
+ end
+
+ before do
+ if klass == Issue
+ params.merge!(issue: { title: "any" })
+ else
+ params.merge!(merge_request: { title: "any" })
+ end
+ end
+
+ context 'when updating causes conflicts' do
+ before do
+ allow_any_instance_of(issuable.class).to receive(:save).
+ and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
+ end
+
+ it 'renders edit when format is html' do
+ put :update, params
+
+ expect(response).to render_template(:edit)
+ expect(assigns[:conflict]).to be_truthy
+ end
+
+ it 'renders json error message when format is json' do
+ params[:format] = "json"
+
+ put :update, params
+
+ expect(response.status).to eq(409)
+ expect(JSON.parse(response.body)).to have_key('errors')
+ end
+ end
+
+ context 'when updating an invalid issuable' do
+ before do
+ key = klass == Issue ? :issue : :merge_request
+ params[key][:title] = ""
+ end
+
+ it 'renders edit when merge request is invalid' do
+ put :update, params
+
+ expect(response).to render_template(:edit)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index df8a47893f9..10458966cb9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -108,7 +108,7 @@ describe 'gitlab:app namespace rake task' do
$stdout = orig_stdout
end
- describe 'backup creation and deletion using annex and custom_hooks' do
+ describe 'backup creation and deletion using custom_hooks' do
let(:project) { create(:project) }
let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
@@ -132,25 +132,6 @@ describe 'gitlab:app namespace rake task' do
Dir.chdir(@origin_cd)
end
- context 'project uses git-annex and successfully creates backup' do
- let(:filename) { "annex" }
-
- it 'creates annex.tar and project bundle' do
- tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
-
- expect(exit_status).to eq(0)
- expect(tar_contents).to match(user_backup_path)
- expect(tar_contents).to match("#{user_backup_path}/annex.tar")
- expect(tar_contents).to match("#{user_backup_path}.bundle")
- end
-
- it 'restores files correctly' do
- restore_backup
-
- expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt")
- end
- end
-
context 'project uses custom_hooks and successfully creates backup' do
let(:filename) { "custom_hooks" }
@@ -246,8 +227,8 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
storages = {
- 'default' => 'tmp/tests/default_storage',
- 'custom' => 'tmp/tests/custom_storage'
+ 'default' => { 'path' => 'tmp/tests/default_storage' },
+ 'custom' => { 'path' => 'tmp/tests/custom_storage' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
new file mode 100644
index 00000000000..ca74378a12a
--- /dev/null
+++ b/spec/tasks/gitlab/info_rake_spec.rb
@@ -0,0 +1,37 @@
+require 'rake_helper'
+
+describe 'gitlab:env:info' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/info'
+
+ stub_warn_user_is_not_gitlab
+ allow(Gitlab::Popen).to receive(:popen)
+ end
+
+ describe 'git version' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version'])
+ .and_return(git_version)
+ end
+
+ context 'when git installed' do
+ let(:git_version) { 'git version 2.10.0' }
+
+ it 'prints git version' do
+ run_rake_task('gitlab:env:info')
+
+ expect($stdout.string).to match(/Git Version:(.*)2.10.0/)
+ end
+ end
+
+ context 'when git not installed' do
+ let(:git_version) { '' }
+
+ it 'prints unknown' do
+ run_rake_task('gitlab:env:info')
+
+ expect($stdout.string).to match(/Git Version:(.*)unknown/)
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 6098be5cd45..ea714fb08f0 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,18 +1,17 @@
require 'spec_helper'
describe AttachmentUploader do
- let(:issue) { build(:issue) }
- subject { described_class.new(issue) }
+ let(:uploader) { described_class.new(build_stubbed(:user)) }
describe '#move_to_cache' do
it 'is true' do
- expect(subject.move_to_cache).to eq(true)
+ expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
- expect(subject.move_to_store).to eq(true)
+ expect(uploader.move_to_store).to eq(true)
end
end
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 76f5a4b42ed..c4d558805ab 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,18 +1,17 @@
require 'spec_helper'
describe AvatarUploader do
- let(:user) { build(:user) }
- subject { described_class.new(user) }
+ let(:uploader) { described_class.new(build_stubbed(:user)) }
describe '#move_to_cache' do
it 'is false' do
- expect(subject.move_to_cache).to eq(false)
+ expect(uploader.move_to_cache).to eq(false)
end
end
describe '#move_to_store' do
it 'is false' do
- expect(subject.move_to_store).to eq(false)
+ expect(uploader.move_to_store).to eq(false)
end
end
end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 6a712e33c96..d9113ef4095 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,57 +1,56 @@
require 'spec_helper'
describe FileUploader do
- let(:project) { create(:project) }
+ let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
- before do
- @previous_enable_processing = FileUploader.enable_processing
- FileUploader.enable_processing = false
- @uploader = FileUploader.new(project)
- end
-
- after do
- FileUploader.enable_processing = @previous_enable_processing
- @uploader.remove!
- end
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ project = build_stubbed(:project)
+ upload = double(model: project, path: 'secret/foo.jpg')
- describe '#image_or_video?' do
- context 'given an image file' do
- before do
- @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
- end
+ dynamic_segment = project.path_with_namespace
- it 'detects an image based on file extension' do
- expect(@uploader.image_or_video?).to be true
- end
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
end
+ end
+
+ describe 'initialize' do
+ it 'generates a secret if none is provided' do
+ expect(SecureRandom).to receive(:hex).and_return('secret')
- context 'given an video file' do
- before do
- video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
- @uploader.store!(video_file)
- end
+ uploader = described_class.new(double)
- it 'detects a video based on file extension' do
- expect(@uploader.image_or_video?).to be true
- end
+ expect(uploader.secret).to eq 'secret'
end
- it 'does not return image_or_video? for other types' do
- @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+ it 'accepts a secret parameter' do
+ expect(SecureRandom).not_to receive(:hex)
- expect(@uploader.image_or_video?).to be false
+ uploader = described_class.new(double, 'secret')
+
+ expect(uploader.secret).to eq 'secret'
end
end
describe '#move_to_cache' do
it 'is true' do
- expect(@uploader.move_to_cache).to eq(true)
+ expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
- expect(@uploader.move_to_store).to eq(true)
+ expect(uploader.move_to_store).to eq(true)
+ end
+ end
+
+ describe '#relative_path' do
+ it 'removes the leading dynamic path segment' do
+ fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ uploader.store!(fixture_file_upload(fixture))
+
+ expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/)
end
end
end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
new file mode 100644
index 00000000000..5c26e334a6e
--- /dev/null
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -0,0 +1,97 @@
+require 'rails_helper'
+
+describe RecordsUploads do
+ let(:uploader) do
+ class RecordsUploadsExampleUploader < GitlabUploader
+ include RecordsUploads
+
+ storage :file
+
+ def model
+ FactoryGirl.build_stubbed(:user)
+ end
+ end
+
+ RecordsUploadsExampleUploader.new
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ end
+
+ describe 'callbacks' do
+ it 'calls `record_upload` after `store`' do
+ expect(uploader).to receive(:record_upload).once
+
+ uploader.store!(upload_fixture('doc_sample.txt'))
+ end
+
+ it 'calls `destroy_upload` after `remove`' do
+ expect(uploader).to receive(:destroy_upload).once
+
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ uploader.remove!
+ end
+ end
+
+ describe '#record_upload callback' do
+ it 'returns early when not using file storage' do
+ allow(uploader).to receive(:file_storage?).and_return(false)
+ expect(Upload).not_to receive(:record)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it "returns early when the file doesn't exist" do
+ allow(uploader).to receive(:file).and_return(double(exists?: false))
+ expect(Upload).not_to receive(:record)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it 'creates an Upload record after store' do
+ expect(Upload).to receive(:record)
+ .with(uploader)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it 'it destroys Upload records at the same path before recording' do
+ existing = Upload.create!(
+ path: File.join('uploads', 'rails_sample.jpg'),
+ size: 512.kilobytes,
+ model: build_stubbed(:user),
+ uploader: uploader.class.to_s
+ )
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(Upload.count).to eq 1
+ end
+ end
+
+ describe '#destroy_upload callback' do
+ it 'returns early when not using file storage' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ allow(uploader).to receive(:file_storage?).and_return(false)
+ expect(Upload).not_to receive(:remove_path)
+
+ uploader.remove!
+ end
+
+ it 'returns early when file is nil' do
+ expect(Upload).not_to receive(:remove_path)
+
+ uploader.remove!
+ end
+
+ it 'it destroys Upload records at the same path after removal' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ expect { uploader.remove! }.to change { Upload.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
new file mode 100644
index 00000000000..c47f09adb6d
--- /dev/null
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+describe UploaderHelper do
+ let(:uploader) do
+ example_uploader = Class.new(CarrierWave::Uploader::Base) do
+ include UploaderHelper
+
+ storage :file
+ end
+
+ example_uploader.new
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ end
+
+ describe '#image_or_video?' do
+ it 'returns true for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'it returns true for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'returns false for other extensions' do
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ expect(uploader).not_to be_image_or_video
+ end
+ end
+end
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
new file mode 100644
index 00000000000..c62450fb8e2
--- /dev/null
+++ b/spec/views/ci/status/_badge.html.haml_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'ci/status/_badge', :view do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when rendering status for build' do
+ let(:build) do
+ create(:ci_build, :success, pipeline: pipeline)
+ end
+
+ context 'when user has ability to see details' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'has link to build details page' do
+ details_path = namespace_project_build_path(
+ project.namespace, project, build)
+
+ render_status(build)
+
+ expect(rendered).to have_link 'passed', href: details_path
+ end
+ end
+
+ context 'when user do not have ability to see build details' do
+ before do
+ render_status(build)
+ end
+
+ it 'contains build status text' do
+ expect(rendered).to have_content 'passed'
+ end
+
+ it 'does not contain links' do
+ expect(rendered).not_to have_link 'passed'
+ end
+ end
+ end
+
+ context 'when rendering status for external job' do
+ context 'when user has ability to see commit status details' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'status has external target url' do
+ before do
+ external_job = create(:generic_commit_status,
+ status: :running,
+ pipeline: pipeline,
+ target_url: 'http://gitlab.com')
+
+ render_status(external_job)
+ end
+
+ it 'contains valid commit status text' do
+ expect(rendered).to have_content 'running'
+ end
+
+ it 'has link to external status page' do
+ expect(rendered).to have_link 'running', href: 'http://gitlab.com'
+ end
+ end
+
+ context 'status do not have external target url' do
+ before do
+ external_job = create(:generic_commit_status, status: :canceled)
+
+ render_status(external_job)
+ end
+
+ it 'contains valid commit status text' do
+ expect(rendered).to have_content 'canceled'
+ end
+
+ it 'has link to external status page' do
+ expect(rendered).not_to have_link 'canceled'
+ end
+ end
+ end
+ end
+
+ def render_status(resource)
+ render 'ci/status/badge', status: resource.detailed_status(user)
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index b6f6e7b7a2b..ec78ac30593 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do
it 'does not show retry button' do
expect(rendered).not_to have_link('Retry')
end
+
+ it 'does not show New issue button' do
+ expect(rendered).not_to have_link('New issue')
+ end
end
context 'when job is not running' do
@@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do
it 'shows retry button' do
expect(rendered).to have_link('Retry')
end
+
+ context 'if build passed' do
+ it 'does not show New issue button' do
+ expect(rendered).not_to have_link('New issue')
+ end
+ end
+
+ context 'if build failed' do
+ before do
+ build.status = 'failed'
+ render
+ end
+
+ it 'shows New issue button' do
+ expect(rendered).to have_link('New issue')
+ end
+ end
end
describe 'commit title in sidebar' do
@@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
+
+ describe 'New issue button' do
+ before do
+ build.status = 'failed'
+ render
+ end
+
+ it 'links to issues/new with the title and description filled in' do
+ title = "Build Failed ##{build.id}"
+ build_url = namespace_project_build_url(project.namespace, project, build)
+ href = new_namespace_project_issue_path(
+ project.namespace,
+ project,
+ issue: {
+ title: title,
+ description: build_url
+ }
+ )
+ expect(rendered).to have_link('New issue', href: href)
+ end
+ end
end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index e741e3cf9b6..8bc344bfbf6 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -3,11 +3,13 @@ require 'spec_helper'
describe 'projects/commit/_commit_box.html.haml' do
include Devise::Test::ControllerHelpers
+ let(:user) { create(:user) }
let(:project) { create(:project) }
before do
assign(:project, project)
assign(:commit, project.commit)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(false)
end
it 'shows the commit SHA' do
@@ -23,6 +25,32 @@ describe 'projects/commit/_commit_box.html.haml' do
render
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ end
+
+ context 'viewing a commit' do
+ context 'as a developer' do
+ before do
+ expect(view).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ it 'has a link to create a new tag' do
+ render
+
+ expect(rendered).to have_link('Tag')
+ end
+ end
+
+ context 'as a non-developer' do
+ before do
+ expect(view).to receive(:can_collaborate_with_project?).and_return(false)
+ end
+
+ it 'does not have a link to create a new tag' do
+ render
+
+ expect(rendered).not_to have_link('Tag')
+ end
+ end
end
end
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index d25de8af5d2..65f9d0125e6 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -50,4 +50,23 @@ describe 'projects/pipelines/_stage', :view do
expect(rendered).to have_text 'test:build', count: 1
end
end
+
+ context 'when there are multiple builds' do
+ before do
+ HasStatus::AVAILABLE_STATUSES.each do |status|
+ create_build(status)
+ end
+ end
+
+ it 'shows them in order' do
+ render
+
+ expect(rendered).to have_text(HasStatus::ORDERED_STATUSES.join(" "))
+ end
+
+ def create_build(status)
+ create(:ci_build, name: status, status: status,
+ pipeline: pipeline, stage: stage.name)
+ end
+ end
end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 97c4bfcd248..bd5cc651c2b 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
describe AuthorizedProjectsWorker do
- let(:worker) { described_class.new }
+ let(:project) { create(:empty_project) }
describe '.bulk_perform_and_wait' do
it 'schedules the ids and waits for the jobs to complete' do
- project = create(:project)
-
project.owner.project_authorizations.delete_all
described_class.bulk_perform_and_wait([[project.owner.id]])
@@ -15,20 +13,37 @@ describe AuthorizedProjectsWorker do
end
end
+ describe '.bulk_perform_async' do
+ it "uses it's respective sidekiq queue" do
+ args = [[project.owner.id]]
+ push_bulk_args = {
+ 'class' => described_class,
+ 'queue' => described_class.sidekiq_options['queue'],
+ 'args' => args
+ }
+
+ expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once
+
+ described_class.bulk_perform_async(args)
+ end
+ end
+
describe '#perform' do
+ subject { described_class.new }
+
it "refreshes user's authorized projects" do
user = create(:user)
expect_any_instance_of(User).to receive(:refresh_authorized_projects)
- worker.perform(user.id)
+ subject.perform(user.id)
end
context "when the user is not found" do
it "does nothing" do
expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
- described_class.new.perform(-1)
+ subject.perform(-1)
end
end
end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
deleted file mode 100644
index 542e674c150..00000000000
--- a/spec/workers/build_email_worker_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe BuildEmailWorker do
- include EmailHelpers
- include RepoHelpers
-
- let(:build) { create(:ci_build) }
- let(:user) { create(:user) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
-
- subject { BuildEmailWorker.new }
-
- before do
- allow(build).to receive(:execute_hooks).and_return(false)
- build.success
- end
-
- describe "#perform" do
- it "sends mail" do
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include('Build success for')
- expect(email.to).to eq([user.email])
- end
-
- it "gracefully handles an input SMTP error" do
- reset_delivered_emails!
- allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
-
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- expect(ActionMailer::Base.deliveries.count).to eq(0)
- end
- end
-end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 5ef8cf1105b..a60af574a08 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -102,8 +102,8 @@ describe GitGarbageCollectWorker do
new_commit_sha = Rugged::Commit.create(
rugged,
message: "hello world #{SecureRandom.hex(6)}",
- author: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
- committer: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
+ author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
+ committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
tree: old_commit.tree,
parents: [old_commit],
)
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 5919b99a6ed..7bcb5521202 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -105,6 +105,6 @@ describe PostReceive do
end
def pwd(project)
- File.join(Gitlab.config.repositories.storages.default, project.path_with_namespace)
+ File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace)
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 60605460adb..87521ae408e 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -15,24 +15,24 @@ describe RepositoryForkWorker do
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
'/test/path',
- project.path_with_namespace,
+ project.full_path,
project.repository_storage_path,
- fork_project.namespace.path
+ fork_project.namespace.full_path
).and_return(true)
subject.perform(
project.id,
'/test/path',
- project.path_with_namespace,
- fork_project.namespace.path)
+ project.full_path,
+ fork_project.namespace.full_path)
end
it 'flushes various caches' do
expect(shell).to receive(:fork_repository).with(
'/test/path',
- project.path_with_namespace,
+ project.full_path,
project.repository_storage_path,
- fork_project.namespace.path
+ fork_project.namespace.full_path
).and_return(true)
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
@@ -41,8 +41,8 @@ describe RepositoryForkWorker do
expect_any_instance_of(Repository).to receive(:expire_exists_cache).
and_call_original
- subject.perform(project.id, '/test/path', project.path_with_namespace,
- fork_project.namespace.path)
+ subject.perform(project.id, '/test/path', project.full_path,
+ fork_project.namespace.full_path)
end
it "handles bad fork" do
@@ -53,8 +53,8 @@ describe RepositoryForkWorker do
subject.perform(
project.id,
'/test/path',
- project.path_with_namespace,
- fork_project.namespace.path)
+ project.full_path,
+ fork_project.namespace.full_path)
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index f1b1574abf4..c42f3147b7a 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -20,7 +20,7 @@ describe RepositoryImportWorker do
context 'when the import has failed' do
it 'hide the credentials that were used in the import URL' do
- error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error })
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
deleted file mode 100644
index 801fa31b45d..00000000000
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require "spec_helper"
-
-describe StuckCiBuildsWorker do
- let!(:build) { create :ci_build }
- let(:worker) { described_class.new }
-
- subject do
- build.reload
- build.status
- end
-
- %w(pending running).each do |status|
- context "#{status} build" do
- before do
- build.update!(status: status)
- end
-
- it 'gets dropped if it was updated over 2 days ago' do
- build.update!(updated_at: 2.days.ago)
- worker.perform
- is_expected.to eq('failed')
- end
-
- it "is still #{status}" do
- build.update!(updated_at: 1.minute.ago)
- worker.perform
- is_expected.to eq(status)
- end
- end
- end
-
- %w(success failed canceled).each do |status|
- context "#{status} build" do
- before do
- build.update!(status: status)
- end
-
- it "is still #{status}" do
- build.update!(updated_at: 2.days.ago)
- worker.perform
- is_expected.to eq(status)
- end
- end
- end
-
- context "for deleted project" do
- before do
- build.update!(status: :running, updated_at: 2.days.ago)
- build.project.update(pending_delete: true)
- end
-
- it "does not drop build" do
- expect_any_instance_of(Ci::Build).not_to receive(:drop)
- worker.perform
- end
- end
-end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
new file mode 100644
index 00000000000..8434b0c8e5b
--- /dev/null
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -0,0 +1,129 @@
+require 'spec_helper'
+
+describe StuckCiJobsWorker do
+ let!(:runner) { create :ci_runner }
+ let!(:job) { create :ci_build, runner: runner }
+ let(:worker) { described_class.new }
+ let(:exclusive_lease_uuid) { SecureRandom.uuid }
+
+ subject do
+ job.reload
+ job.status
+ end
+
+ before do
+ job.update!(status: status, updated_at: updated_at)
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
+ end
+
+ shared_examples 'job is dropped' do
+ it 'changes status' do
+ worker.perform
+ is_expected.to eq('failed')
+ end
+ end
+
+ shared_examples 'job is unchanged' do
+ it "doesn't change status" do
+ worker.perform
+ is_expected.to eq(status)
+ end
+ end
+
+ context 'when job is pending' do
+ let(:status) { 'pending' }
+
+ context 'when job is not stuck' do
+ before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) }
+
+ context 'when job was not updated for more than 1 day ago' do
+ let(:updated_at) { 2.days.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 day ago' do
+ let(:updated_at) { 6.hours.ago }
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ context 'when job is stuck' do
+ before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) }
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 hour ago' do
+ let(:updated_at) { 30.minutes.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+ end
+
+ context 'when job is running' do
+ let(:status) { 'running' }
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 hour ago' do
+ let(:updated_at) { 30.minutes.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ %w(success skipped failed canceled).each do |status|
+ context "when job is #{status}" do
+ let(:status) { status }
+ let(:updated_at) { 2.days.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ context 'for deleted project' do
+ let(:status) { 'running' }
+ let(:updated_at) { 2.days.ago }
+
+ before { job.project.update(pending_delete: true) }
+
+ it 'does not drop job' do
+ expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ worker.perform
+ end
+ end
+
+ describe 'exclusive lease' do
+ let(:status) { 'running' }
+ let(:updated_at) { 2.days.ago }
+ let(:worker2) { described_class.new }
+
+ it 'is guard by exclusive lease when executed concurrently' do
+ expect(worker).to receive(:drop).at_least(:once)
+ expect(worker2).not_to receive(:drop)
+ worker.perform
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
+ worker2.perform
+ end
+
+ it 'can be executed in sequence' do
+ expect(worker).to receive(:drop).at_least(:once)
+ expect(worker2).to receive(:drop).at_least(:once)
+ worker.perform
+ worker2.perform
+ end
+
+ it 'cancels exclusive lease after worker perform' do
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).with(described_class::EXCLUSIVE_LEASE_KEY, exclusive_lease_uuid)
+ worker.perform
+ end
+ end
+end
diff --git a/spec/workers/system_hook_push_worker_spec.rb b/spec/workers/system_hook_push_worker_spec.rb
new file mode 100644
index 00000000000..b1d446ed25f
--- /dev/null
+++ b/spec/workers/system_hook_push_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe SystemHookPushWorker do
+ include RepoHelpers
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'executes SystemHooksService with expected values' do
+ push_data = double('push_data')
+ system_hook_service = double('system_hook_service')
+
+ expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
+
+ subject.perform(push_data, :push_hooks)
+ end
+ end
+end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index c78a69eda67..262d6e5a9ab 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -23,16 +23,5 @@ describe UpdateMergeRequestsWorker do
perform
end
-
- it 'executes SystemHooksService with expected values' do
- push_data = double('push_data')
- expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data)
-
- system_hook_service = double('system_hook_service')
- expect(SystemHooksService).to receive(:new).and_return(system_hook_service)
- expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks)
-
- perform
- end
end
end
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
new file mode 100644
index 00000000000..911360da66c
--- /dev/null
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe UploadChecksumWorker do
+ describe '#perform' do
+ it 'rescues ActiveRecord::RecordNotFound' do
+ expect { described_class.new.perform(999_999) }.not_to raise_error
+ end
+
+ it 'calls calculate_checksum_without_delay and save!' do
+ upload = spy
+ expect(Upload).to receive(:find).with(999_999).and_return(upload)
+
+ described_class.new.perform(999_999)
+
+ expect(upload).to have_received(:calculate_checksum)
+ expect(upload).to have_received(:save!)
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js
deleted file mode 100644
index 166bd654d6e..00000000000
--- a/vendor/assets/javascripts/g.bar.js
+++ /dev/null
@@ -1,674 +0,0 @@
-/*!
- * g.Raphael 0.51 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- */
-(function () {
- var mmin = Math.min,
- mmax = Math.max;
-
- function finger(x, y, width, height, dir, ending, isPath, paper) {
- var path,
- ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' };
-
- // dir 0 for horizontal and 1 for vertical
- if ((dir && !height) || (!dir && !width)) {
- return isPath ? "" : paper.path();
- }
-
- ending = ends[ending] || "square";
- height = Math.round(height);
- width = Math.round(width);
- x = Math.round(x);
- y = Math.round(y);
-
- switch (ending) {
- case "round":
- if (!dir) {
- var r = ~~(height / 2);
-
- if (width < r) {
- r = width;
- path = [
- "M", x + .5, y + .5 - ~~(height / 2),
- "l", 0, 0,
- "a", r, ~~(height / 2), 0, 0, 1, 0, height,
- "l", 0, 0,
- "z"
- ];
- } else {
- path = [
- "M", x + .5, y + .5 - r,
- "l", width - r, 0,
- "a", r, r, 0, 1, 1, 0, height,
- "l", r - width, 0,
- "z"
- ];
- }
- } else {
- r = ~~(width / 2);
-
- if (height < r) {
- r = height;
- path = [
- "M", x - ~~(width / 2), y,
- "l", 0, 0,
- "a", ~~(width / 2), r, 0, 0, 1, width, 0,
- "l", 0, 0,
- "z"
- ];
- } else {
- path = [
- "M", x - r, y,
- "l", 0, r - height,
- "a", r, r, 0, 1, 1, width, 0,
- "l", 0, height - r,
- "z"
- ];
- }
- }
- break;
- case "sharp":
- if (!dir) {
- var half = ~~(height / 2);
-
- path = [
- "M", x, y + half,
- "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height),
- "z"
- ];
- } else {
- half = ~~(width / 2);
- path = [
- "M", x + half, y,
- "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half,
- "z"
- ];
- }
- break;
- case "square":
- if (!dir) {
- path = [
- "M", x, y + ~~(height / 2),
- "l", 0, -height, width, 0, 0, height,
- "z"
- ];
- } else {
- path = [
- "M", x + ~~(width / 2), y,
- "l", 1 - width, 0, 0, -height, width - 1, 0,
- "z"
- ];
- }
- break;
- case "soft":
- if (!dir) {
- r = mmin(width, Math.round(height / 5));
- path = [
- "M", x + .5, y + .5 - ~~(height / 2),
- "l", width - r, 0,
- "a", r, r, 0, 0, 1, r, r,
- "l", 0, height - r * 2,
- "a", r, r, 0, 0, 1, -r, r,
- "l", r - width, 0,
- "z"
- ];
- } else {
- r = mmin(Math.round(width / 5), height);
- path = [
- "M", x - ~~(width / 2), y,
- "l", 0, r - height,
- "a", r, r, 0, 0, 1, r, -r,
- "l", width - 2 * r, 0,
- "a", r, r, 0, 0, 1, r, r,
- "l", 0, height - r,
- "z"
- ];
- }
- }
-
- if (isPath) {
- return path.join(",");
- } else {
- return paper.path(path);
- }
- }
-
-/*\
- * Paper.vbarchart
- [ method ]
- **
- * Creates a vertical bar chart
- **
- > Parameters
- **
- - x (number) x coordinate of the chart
- - y (number) y coordinate of the chart
- - width (number) width of the chart (respected by all elements in the set)
- - height (number) height of the chart (respected by all elements in the set)
- - values (array) values
- - opts (object) options for the chart
- o {
- o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
- o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
- o vgutter (number)
- o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
- o stacked (boolean) whether or not to tread values as in a stacked bar chart
- o to
- o stretch (boolean)
- o }
- **
- = (object) path element of the popup
- > Usage
- | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
- \*/
-
- function VBarchart(paper, x, y, width, height, values, opts) {
- opts = opts || {};
-
- var chartinst = this,
- type = opts.type || "square",
- gutter = parseFloat(opts.gutter || "20%"),
- chart = paper.set(),
- bars = paper.set(),
- covers = paper.set(),
- covers2 = paper.set(),
- total = Math.max.apply(Math, values),
- stacktotal = [],
- multi = 0,
- colors = opts.colors || chartinst.colors,
- len = values.length;
-
- if (Raphael.is(values[0], "array")) {
- total = [];
- multi = len;
- len = 0;
-
- for (var i = values.length; i--;) {
- bars.push(paper.set());
- total.push(Math.max.apply(Math, values[i]));
- len = Math.max(len, values[i].length);
- }
-
- if (opts.stacked) {
- for (var i = len; i--;) {
- var tot = 0;
-
- for (var j = values.length; j--;) {
- tot +=+ values[j][i] || 0;
- }
-
- stacktotal.push(tot);
- }
- }
-
- for (var i = values.length; i--;) {
- if (values[i].length < len) {
- for (var j = len; j--;) {
- values[i].push(0);
- }
- }
- }
-
- total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
- }
-
- total = (opts.to) || total;
-
- var barwidth = width / (len * (100 + gutter) + gutter) * 100,
- barhgutter = barwidth * gutter / 100,
- barvgutter = opts.vgutter == null ? 20 : opts.vgutter,
- stack = [],
- X = x + barhgutter,
- Y = (height - 2 * barvgutter) / total;
-
- if (!opts.stretch) {
- barhgutter = Math.round(barhgutter);
- barwidth = Math.floor(barwidth);
- }
-
- !opts.stacked && (barwidth /= multi || 1);
-
- for (var i = 0; i < len; i++) {
- stack = [];
-
- for (var j = 0; j < (multi || 1); j++) {
- var h = Math.round((multi ? values[j][i] : values[i]) * Y),
- top = y + height - barvgutter - h,
- bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] });
-
- if (multi) {
- bars[j].push(bar);
- } else {
- bars.push(bar);
- }
-
- bar.y = top;
- bar.x = Math.round(X + barwidth / 2);
- bar.w = barwidth;
- bar.h = h;
- bar.value = multi ? values[j][i] : values[i];
-
- if (!opts.stacked) {
- X += barwidth;
- } else {
- stack.push(bar);
- }
- }
-
- if (opts.stacked) {
- var cvr;
-
- covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim));
- cvr.bars = paper.set();
-
- var size = 0;
-
- for (var s = stack.length; s--;) {
- stack[s].toFront();
- }
-
- for (var s = 0, ss = stack.length; s < ss; s++) {
- var bar = stack[s],
- cover,
- h = (size + bar.value) * Y,
- path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper);
-
- cvr.bars.push(bar);
- size && bar.attr({path: path});
- bar.h = h;
- bar.y = y + height - barvgutter - !!size * .5 - h;
- covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim));
- cover.bar = bar;
- cover.value = bar.value;
- size += bar.value;
- }
-
- X += barwidth;
- }
-
- X += barhgutter;
- }
-
- covers2.toFront();
- X = x + barhgutter;
-
- if (!opts.stacked) {
- for (var i = 0; i < len; i++) {
- for (var j = 0; j < (multi || 1); j++) {
- var cover;
-
- covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim));
- cover.bar = multi ? bars[j][i] : bars[i];
- cover.value = cover.bar.value;
- X += barwidth;
- }
-
- X += barhgutter;
- }
- }
-
- chart.label = function (labels, isBottom) {
- labels = labels || [];
- this.labels = paper.set();
-
- var L, l = -Infinity;
-
- if (opts.stacked) {
- for (var i = 0; i < len; i++) {
- var tot = 0;
-
- for (var j = 0; j < (multi || 1); j++) {
- tot += multi ? values[j][i] : values[i];
-
- if (j == multi - 1) {
- var label = paper.labelise(labels[i], tot, total);
-
- L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
-
- var bb = L.getBBox();
-
- if (bb.x - 7 < l) {
- L.remove();
- } else {
- this.labels.push(L);
- l = bb.x + bb.width;
- }
- }
- }
- }
- } else {
- for (var i = 0; i < len; i++) {
- for (var j = 0; j < (multi || 1); j++) {
- var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
-
- L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]);
-
- var bb = L.getBBox();
-
- if (bb.x - 7 < l) {
- L.remove();
- } else {
- this.labels.push(L);
- l = bb.x + bb.width;
- }
- }
- }
- }
- return this;
- };
-
- chart.hover = function (fin, fout) {
- covers2.hide();
- covers.show();
- covers.mouseover(fin).mouseout(fout);
- return this;
- };
-
- chart.hoverColumn = function (fin, fout) {
- covers.hide();
- covers2.show();
- fout = fout || function () {};
- covers2.mouseover(fin).mouseout(fout);
- return this;
- };
-
- chart.click = function (f) {
- covers2.hide();
- covers.show();
- covers.click(f);
- return this;
- };
-
- chart.each = function (f) {
- if (!Raphael.is(f, "function")) {
- return this;
- }
- for (var i = covers.length; i--;) {
- f.call(covers[i]);
- }
- return this;
- };
-
- chart.eachColumn = function (f) {
- if (!Raphael.is(f, "function")) {
- return this;
- }
- for (var i = covers2.length; i--;) {
- f.call(covers2[i]);
- }
- return this;
- };
-
- chart.clickColumn = function (f) {
- covers.hide();
- covers2.show();
- covers2.click(f);
- return this;
- };
-
- chart.push(bars, covers, covers2);
- chart.bars = bars;
- chart.covers = covers;
- return chart;
- };
-
- //inheritance
- var F = function() {};
- F.prototype = Raphael.g;
- HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart
-
- Raphael.fn.barchart = function(x, y, width, height, values, opts) {
- return new VBarchart(this, x, y, width, height, values, opts);
- };
-
-/*\
- * Paper.barchart
- [ method ]
- **
- * Creates a horizontal bar chart
- **
- > Parameters
- **
- - x (number) x coordinate of the chart
- - y (number) y coordinate of the chart
- - width (number) width of the chart (respected by all elements in the set)
- - height (number) height of the chart (respected by all elements in the set)
- - values (array) values
- - opts (object) options for the chart
- o {
- o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'.
- o gutter (number)(string) default '20%' (WHAT DOES IT DO?)
- o vgutter (number)
- o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color.
- o stacked (boolean) whether or not to tread values as in a stacked bar chart
- o to
- o stretch (boolean)
- o }
- **
- = (object) path element of the popup
- > Usage
- | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {})
- \*/
-
- function HBarchart(paper, x, y, width, height, values, opts) {
- opts = opts || {};
-
- var chartinst = this,
- type = opts.type || "square",
- gutter = parseFloat(opts.gutter || "20%"),
- chart = paper.set(),
- bars = paper.set(),
- covers = paper.set(),
- covers2 = paper.set(),
- total = Math.max.apply(Math, values),
- stacktotal = [],
- multi = 0,
- colors = opts.colors || chartinst.colors,
- len = values.length;
-
- if (Raphael.is(values[0], "array")) {
- total = [];
- multi = len;
- len = 0;
-
- for (var i = values.length; i--;) {
- bars.push(paper.set());
- total.push(Math.max.apply(Math, values[i]));
- len = Math.max(len, values[i].length);
- }
-
- if (opts.stacked) {
- for (var i = len; i--;) {
- var tot = 0;
- for (var j = values.length; j--;) {
- tot +=+ values[j][i] || 0;
- }
- stacktotal.push(tot);
- }
- }
-
- for (var i = values.length; i--;) {
- if (values[i].length < len) {
- for (var j = len; j--;) {
- values[i].push(0);
- }
- }
- }
-
- total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
- }
-
- total = (opts.to) || total;
-
- var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
- bargutter = Math.floor(barheight * gutter / 100),
- stack = [],
- Y = y + bargutter,
- X = (width - 1) / total;
-
- !opts.stacked && (barheight /= multi || 1);
-
- for (var i = 0; i < len; i++) {
- stack = [];
-
- for (var j = 0; j < (multi || 1); j++) {
- var val = multi ? values[j][i] : values[i],
- bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]});
-
- if (multi) {
- bars[j].push(bar);
- } else {
- bars.push(bar);
- }
-
- bar.x = x + Math.round(val * X);
- bar.y = Y + barheight / 2;
- bar.w = Math.round(val * X);
- bar.h = barheight;
- bar.value = +val;
-
- if (!opts.stacked) {
- Y += barheight;
- } else {
- stack.push(bar);
- }
- }
-
- if (opts.stacked) {
- var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim);
-
- covers2.push(cvr);
- cvr.bars = paper.set();
-
- var size = 0;
-
- for (var s = stack.length; s--;) {
- stack[s].toFront();
- }
-
- for (var s = 0, ss = stack.length; s < ss; s++) {
- var bar = stack[s],
- cover,
- val = Math.round((size + bar.value) * X),
- path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper);
-
- cvr.bars.push(bar);
- size && bar.attr({ path: path });
- bar.w = val;
- bar.x = x + val;
- covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim));
- cover.bar = bar;
- size += bar.value;
- }
-
- Y += barheight;
- }
-
- Y += bargutter;
- }
-
- covers2.toFront();
- Y = y + bargutter;
-
- if (!opts.stacked) {
- for (var i = 0; i < len; i++) {
- for (var j = 0; j < (multi || 1); j++) {
- var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim);
-
- covers.push(cover);
- cover.bar = multi ? bars[j][i] : bars[i];
- cover.value = cover.bar.value;
- Y += barheight;
- }
-
- Y += bargutter;
- }
- }
-
- chart.label = function (labels, isRight) {
- labels = labels || [];
- this.labels = paper.set();
-
- for (var i = 0; i < len; i++) {
- for (var j = 0; j < multi; j++) {
- var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total),
- X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
- A = isRight ? "end" : "start",
- L;
-
- this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0]));
-
- if (L.getBBox().x < x + 5) {
- L.attr({x: x + 5, "text-anchor": "start"});
- } else {
- bars[i * (multi || 1) + j].label = L;
- }
- }
- }
-
- return this;
- };
-
- chart.hover = function (fin, fout) {
- covers2.hide();
- covers.show();
- fout = fout || function () {};
- covers.mouseover(fin).mouseout(fout);
- return this;
- };
-
- chart.hoverColumn = function (fin, fout) {
- covers.hide();
- covers2.show();
- fout = fout || function () {};
- covers2.mouseover(fin).mouseout(fout);
- return this;
- };
-
- chart.each = function (f) {
- if (!Raphael.is(f, "function")) {
- return this;
- }
- for (var i = covers.length; i--;) {
- f.call(covers[i]);
- }
- return this;
- };
-
- chart.eachColumn = function (f) {
- if (!Raphael.is(f, "function")) {
- return this;
- }
- for (var i = covers2.length; i--;) {
- f.call(covers2[i]);
- }
- return this;
- };
-
- chart.click = function (f) {
- covers2.hide();
- covers.show();
- covers.click(f);
- return this;
- };
-
- chart.clickColumn = function (f) {
- covers.hide();
- covers2.show();
- covers2.click(f);
- return this;
- };
-
- chart.push(bars, covers, covers2);
- chart.bars = bars;
- chart.covers = covers;
- return chart;
- };
-
- Raphael.fn.hbarchart = function(x, y, width, height, values, opts) {
- return new HBarchart(this, x, y, width, height, values, opts);
- };
-
-})();
diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js
deleted file mode 100644
index 27f27caf9f2..00000000000
--- a/vendor/assets/javascripts/g.raphael.js
+++ /dev/null
@@ -1,861 +0,0 @@
-/*!
- * g.Raphael 0.51 - Charting library, based on Raphaël
- *
- * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com)
- * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
- */
-
-/*
- * Tooltips on Element prototype
- */
-/*\
- * Element.popup
- [ method ]
- **
- * Puts the context Element in a 'popup' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
- - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`]
- - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`]
- - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`]
- **
- = (object) path element of the popup
- \*/
-Raphael.el.popup = function (dir, size, x, y) {
- var paper = this.paper || this[0].paper,
- bb, xy, center, cw, ch;
-
- if (!paper) return;
-
- switch (this.type) {
- case 'text':
- case 'circle':
- case 'ellipse': center = true; break;
- default: center = false;
- }
-
- dir = dir == null ? 'up' : dir;
- size = size || 5;
- bb = this.getBBox();
-
- x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
- y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
- cw = Math.max(bb.width / 2 - size, 0);
- ch = Math.max(bb.height / 2 - size, 0);
-
- this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0));
- bb = this.getBBox();
-
- var paths = {
- up: [
- 'M', x, y,
- 'l', -size, -size, -cw, 0,
- 'a', size, size, 0, 0, 1, -size, -size,
- 'l', 0, -bb.height,
- 'a', size, size, 0, 0, 1, size, -size,
- 'l', size * 2 + cw * 2, 0,
- 'a', size, size, 0, 0, 1, size, size,
- 'l', 0, bb.height,
- 'a', size, size, 0, 0, 1, -size, size,
- 'l', -cw, 0,
- 'z'
- ].join(','),
- down: [
- 'M', x, y,
- 'l', size, size, cw, 0,
- 'a', size, size, 0, 0, 1, size, size,
- 'l', 0, bb.height,
- 'a', size, size, 0, 0, 1, -size, size,
- 'l', -(size * 2 + cw * 2), 0,
- 'a', size, size, 0, 0, 1, -size, -size,
- 'l', 0, -bb.height,
- 'a', size, size, 0, 0, 1, size, -size,
- 'l', cw, 0,
- 'z'
- ].join(','),
- left: [
- 'M', x, y,
- 'l', -size, size, 0, ch,
- 'a', size, size, 0, 0, 1, -size, size,
- 'l', -bb.width, 0,
- 'a', size, size, 0, 0, 1, -size, -size,
- 'l', 0, -(size * 2 + ch * 2),
- 'a', size, size, 0, 0, 1, size, -size,
- 'l', bb.width, 0,
- 'a', size, size, 0, 0, 1, size, size,
- 'l', 0, ch,
- 'z'
- ].join(','),
- right: [
- 'M', x, y,
- 'l', size, -size, 0, -ch,
- 'a', size, size, 0, 0, 1, size, -size,
- 'l', bb.width, 0,
- 'a', size, size, 0, 0, 1, size, size,
- 'l', 0, size * 2 + ch * 2,
- 'a', size, size, 0, 0, 1, -size, size,
- 'l', -bb.width, 0,
- 'a', size, size, 0, 0, 1, -size, -size,
- 'l', 0, -ch,
- 'z'
- ].join(',')
- };
-
- xy = {
- up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) },
- down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) },
- left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) },
- right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }
- }[dir];
-
- this.translate(xy.x, xy.y);
- return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.tag
- [ method ]
- **
- * Puts the context Element in a 'tag' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - r (number) radius of the loop [default: `5`]
- - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`]
- - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`]
- **
- = (object) path element of the tag
- \*/
-Raphael.el.tag = function (angle, r, x, y) {
- var d = 3,
- paper = this.paper || this[0].paper;
-
- if (!paper) return;
-
- var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
- bb = this.getBBox(),
- dx, R, center, tmp;
-
- switch (this.type) {
- case 'text':
- case 'circle':
- case 'ellipse': center = true; break;
- default: center = false;
- }
-
- angle = angle || 0;
- x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
- y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
- r = r == null ? 5 : r;
- R = .5522 * r;
-
- if (bb.height >= r * 2) {
- p.attr({
- path: [
- "M", x, y + r,
- "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
- "m", 0, -r * 2 -d,
- "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2,
- "L", x + r + d, y + bb.height / 2 + d,
- "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0,
- "L", x, y - r - d
- ].join(",")
- });
- } else {
- dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
- p.attr({
- path: [
- "M", x, y + r,
- "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r,
- "M", x + dx, y - bb.height / 2 - d,
- "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d,
- "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d,
- "L", x + dx, y - bb.height / 2 - d
- ].join(",")
- });
- }
-
- angle = 360 - angle;
- p.rotate(angle, x, y);
-
- if (this.attrs) {
- //elements
- this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
- this.rotate(angle, x, y);
- angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
- } else {
- //sets
- if (angle > 90 && angle < 270) {
- this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2);
- this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2);
- } else {
- this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2);
- this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2);
- }
- }
-
- return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.drop
- [ method ]
- **
- * Puts the context Element in a 'drop' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`]
- - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`]
- **
- = (object) path element of the drop
- \*/
-Raphael.el.drop = function (angle, x, y) {
- var bb = this.getBBox(),
- paper = this.paper || this[0].paper,
- center, size, p, dx, dy;
-
- if (!paper) return;
-
- switch (this.type) {
- case 'text':
- case 'circle':
- case 'ellipse': center = true; break;
- default: center = false;
- }
-
- angle = angle || 0;
-
- x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
- y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
- size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height);
- p = paper.path([
- "M", x, y,
- "l", size, 0,
- "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7,
- "z"
- ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y);
-
- angle = (angle + 90) * Math.PI / 180;
- dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2);
- dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2);
-
- this.attrs ?
- this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) :
- this.translate(dx - bb.x, dy - bb.y);
-
- return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.flag
- [ method ]
- **
- * Puts the context Element in a 'flag' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`]
- - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`]
- **
- = (object) path element of the flag
- \*/
-Raphael.el.flag = function (angle, x, y) {
- var d = 3,
- paper = this.paper || this[0].paper;
-
- if (!paper) return;
-
- var p = paper.path().attr({ fill: '#000', stroke: '#000' }),
- bb = this.getBBox(),
- h = bb.height / 2,
- center;
-
- switch (this.type) {
- case 'text':
- case 'circle':
- case 'ellipse': center = true; break;
- default: center = false;
- }
-
- angle = angle || 0;
- x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
- y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y);
-
- p.attr({
- path: [
- "M", x, y,
- "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0,
- "z"
- ].join(",")
- });
-
- angle = 360 - angle;
- p.rotate(angle, x, y);
-
- if (this.attrs) {
- //elements
- this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2);
- this.rotate(angle, x, y);
- angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y);
- } else {
- //sets
- if (angle > 90 && angle < 270) {
- this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2);
- this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2);
- } else {
- this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2);
- this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2);
- }
- }
-
- return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.label
- [ method ]
- **
- * Puts the context Element in a 'label' tooltip. Can also be used on sets.
- **
- = (object) path element of the label.
- \*/
-Raphael.el.label = function () {
- var bb = this.getBBox(),
- paper = this.paper || this[0].paper,
- r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
-
- if (!paper) return;
-
- return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]);
-};
-
-/*\
- * Element.blob
- [ method ]
- **
- * Puts the context Element in a 'blob' tooltip. Can also be used on sets.
- **
- > Parameters
- **
- - angle (number) angle of orientation in degrees [default: `0`]
- - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`]
- - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`]
- **
- = (object) path element of the blob
- \*/
-Raphael.el.blob = function (angle, x, y) {
- var bb = this.getBBox(),
- rad = Math.PI / 180,
- paper = this.paper || this[0].paper,
- p, center, size;
-
- if (!paper) return;
-
- switch (this.type) {
- case 'text':
- case 'circle':
- case 'ellipse': center = true; break;
- default: center = false;
- }
-
- p = paper.path().attr({ fill: "#000", stroke: "none" });
- angle = (+angle + 1 ? angle : 45) + 90;
- size = Math.min(bb.height, bb.width);
- x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x);
- y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y);
-
- var w = Math.max(bb.width + size, size * 25 / 12),
- h = Math.max(bb.height + size, size * 25 / 12),
- x2 = x + size * Math.sin((angle - 22.5) * rad),
- y2 = y + size * Math.cos((angle - 22.5) * rad),
- x1 = x + size * Math.sin((angle + 22.5) * rad),
- y1 = y + size * Math.cos((angle + 22.5) * rad),
- dx = (x1 - x2) / 2,
- dy = (y1 - y2) / 2,
- rx = w / 2,
- ry = h / 2,
- k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
- cx = k * rx * dy / ry + (x1 + x2) / 2,
- cy = k * -ry * dx / rx + (y1 + y2) / 2;
-
- p.attr({
- x: cx,
- y: cy,
- path: [
- "M", x, y,
- "L", x1, y1,
- "A", rx, ry, 0, 1, 1, x2, y2,
- "z"
- ].join(",")
- });
-
- this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2);
-
- return p.insertBefore(this.node ? this : this[0]);
-};
-
-/*
- * Tooltips on Paper prototype
- */
-/*\
- * Paper.label
- [ method ]
- **
- * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label
- **
- > Parameters
- **
- - x (number) x coordinate of the center of the label
- - y (number) y coordinate of the center of the label
- - text (string) text to place inside the label
- **
- = (object) set containing the label path and the text element
- > Usage
- | paper.label(50, 50, "$9.99");
- \*/
-Raphael.fn.label = function (x, y, text) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.label(), text);
-};
-
-/*\
- * Paper.popup
- [ method ]
- **
- * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup
- *
- * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively.
- **
- > Parameters
- **
- - x (number) x coordinate of the popup's tail
- - y (number) y coordinate of the popup's tail
- - text (string) text to place inside the popup
- - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`.
- - size (number) amount of padding around the Element [default: `5`]
- **
- = (object) set containing the popup path and the text element
- > Usage
- | paper.popup(50, 50, "$9.99", 'down');
- \*/
-Raphael.fn.popup = function (x, y, text, dir, size) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.popup(dir, size), text);
-};
-
-/*\
- * Paper.tag
- [ method ]
- **
- * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag
- **
- > Parameters
- **
- - x (number) x coordinate of the center of the tag loop
- - y (number) y coordinate of the center of the tag loop
- - text (string) text to place inside the tag
- - angle (number) angle of orientation in degrees [default: `0`]
- - r (number) radius of the loop [default: `5`]
- **
- = (object) set containing the tag path and the text element
- > Usage
- | paper.tag(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.tag = function (x, y, text, angle, r) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.tag(angle, r), text);
-};
-
-/*\
- * Paper.flag
- [ method ]
- **
- * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag
- **
- > Parameters
- **
- - x (number) x coordinate of the flag's point
- - y (number) y coordinate of the flag's point
- - text (string) text to place inside the flag
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the flag path and the text element
- > Usage
- | paper.flag(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.flag = function (x, y, text, angle) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.flag(angle), text);
-};
-
-/*\
- * Paper.drop
- [ method ]
- **
- * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop
- **
- > Parameters
- **
- - x (number) x coordinate of the drop's point
- - y (number) y coordinate of the drop's point
- - text (string) text to place inside the drop
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the drop path and the text element
- > Usage
- | paper.drop(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.drop = function (x, y, text, angle) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.drop(angle), text);
-};
-
-/*\
- * Paper.blob
- [ method ]
- **
- * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob
- **
- > Parameters
- **
- - x (number) x coordinate of the blob's tail
- - y (number) y coordinate of the blob's tail
- - text (string) text to place inside the blob
- - angle (number) angle of orientation in degrees [default: `0`]
- **
- = (object) set containing the blob path and the text element
- > Usage
- | paper.blob(50, 50, "$9.99", 60);
- \*/
-Raphael.fn.blob = function (x, y, text, angle) {
- var set = this.set();
-
- text = this.text(x, y, text).attr(Raphael.g.txtattr);
- return set.push(text.blob(angle), text);
-};
-
-/**
- * Brightness functions on the Element prototype
- */
-/*\
- * Element.lighter
- [ method ]
- **
- * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets.
- **
- > Parameters
- **
- - times (number) adjustment factor [default: `2`]
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- | fill: "#ff0000",
- | stroke: "#fff",
- | "stroke-width": 2
- | }).lighter(6);
- \*/
-Raphael.el.lighter = function (times) {
- times = times || 2;
-
- var fs = [this.attrs.fill, this.attrs.stroke];
-
- this.fs = this.fs || [fs[0], fs[1]];
-
- fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
- fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
- fs[0].b = Math.min(fs[0].b * times, 1);
- fs[0].s = fs[0].s / times;
- fs[1].b = Math.min(fs[1].b * times, 1);
- fs[1].s = fs[1].s / times;
-
- this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
- return this;
-};
-
-/*\
- * Element.darker
- [ method ]
- **
- * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets.
- **
- > Parameters
- **
- - times (number) adjustment factor [default: `2`]
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- | fill: "#ff0000",
- | stroke: "#fff",
- | "stroke-width": 2
- | }).darker(6);
- \*/
-Raphael.el.darker = function (times) {
- times = times || 2;
-
- var fs = [this.attrs.fill, this.attrs.stroke];
-
- this.fs = this.fs || [fs[0], fs[1]];
-
- fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
- fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
- fs[0].s = Math.min(fs[0].s * times, 1);
- fs[0].b = fs[0].b / times;
- fs[1].s = Math.min(fs[1].s * times, 1);
- fs[1].b = fs[1].b / times;
-
- this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
- return this;
-};
-
-/*\
- * Element.resetBrightness
- [ method ]
- **
- * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets.
- **
- = (object) Element
- > Usage
- | paper.circle(50, 50, 20).attr({
- | fill: "#ff0000",
- | stroke: "#fff",
- | "stroke-width": 2
- | }).lighter(6).resetBrightness();
- \*/
-Raphael.el.resetBrightness = function () {
- if (this.fs) {
- this.attr({ fill: this.fs[0], stroke: this.fs[1] });
- delete this.fs;
- }
- return this;
-};
-
-//alias to set prototype
-(function () {
- var brightness = ['lighter', 'darker', 'resetBrightness'],
- tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob'];
-
- for (var f in tooltips) (function (name) {
- Raphael.st[name] = function () {
- return Raphael.el[name].apply(this, arguments);
- };
- })(tooltips[f]);
-
- for (var f in brightness) (function (name) {
- Raphael.st[name] = function () {
- for (var i = 0; i < this.length; i++) {
- this[i][name].apply(this[i], arguments);
- }
-
- return this;
- };
- })(brightness[f]);
-})();
-
-//chart prototype for storing common functions
-Raphael.g = {
- /*\
- * g.shim
- [ object ]
- **
- * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to)
- **
- > Default value
- | { stroke: 'none', fill: '#000', 'fill-opacity': 0 }
- \*/
- shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 },
-
- /*\
- * g.txtattr
- [ object ]
- **
- * An attribute object that charts and tooltips will set on any generated text
- **
- > Default value
- | { font: '12px Arial, sans-serif', fill: '#fff' }
- \*/
- txtattr: { font: '12px Arial, sans-serif', fill: '#fff' },
-
- /*\
- * g.colors
- [ array ]
- **
- * An array of color values that charts will iterate through when drawing chart data values.
- **
- \*/
- colors: (function () {
- var hues = [.6, .2, .05, .1333, .75, 0],
- colors = [];
-
- for (var i = 0; i < 10; i++) {
- if (i < hues.length) {
- colors.push('hsb(' + hues[i] + ',.75, .75)');
- } else {
- colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)');
- }
- }
-
- return colors;
- })(),
-
- snapEnds: function(from, to, steps) {
- var f = from,
- t = to;
-
- if (f == t) {
- return {from: f, to: t, power: 0};
- }
-
- function round(a) {
- return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a);
- }
-
- var d = (t - f) / steps,
- r = ~~(d),
- R = r,
- i = 0;
-
- if (r) {
- while (R) {
- i--;
- R = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
- }
-
- i ++;
- } else {
- if(d == 0 || !isFinite(d)) {
- i = 1;
- } else {
- while (!r) {
- i = i || 1;
- r = ~~(d * Math.pow(10, i)) / Math.pow(10, i);
- i++;
- }
- }
-
- i && i--;
- }
-
- t = round(to * Math.pow(10, i)) / Math.pow(10, i);
-
- if (t < to) {
- t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
- }
-
- f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
- return { from: f, to: t, power: i };
- },
-
- axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) {
- dashsize = dashsize == null ? 2 : dashsize;
- type = type || "t";
- steps = steps || 10;
- paper = arguments[arguments.length-1] //paper is always last argument
-
- var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
- ends = this.snapEnds(from, to, steps),
- f = ends.from,
- t = ends.to,
- i = ends.power,
- j = 0,
- txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" },
- text = paper.set(),
- d;
-
- d = (t - f) / steps;
-
- var label = f,
- rnd = i > 0 ? i : 0;
- dx = length / steps;
-
- if (+orientation == 1 || +orientation == 3) {
- var Y = y,
- addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
-
- while (Y >= y - length) {
- type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
- text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
- label += d;
- Y -= dx;
- }
-
- if (Math.round(Y + dx - (y - length))) {
- type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
- text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" }));
- }
- } else {
- label = f;
- rnd = (i > 0) * i;
- addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation);
-
- var X = x,
- dx = length / steps,
- txt = 0,
- prev = 0;
-
- while (X <= x + length) {
- type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
- text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
-
- var bb = txt.getBBox();
-
- if (prev >= bb.x - 5) {
- text.pop(text.length - 1).remove();
- } else {
- prev = bb.x + bb.width;
- }
-
- label += d;
- X += dx;
- }
-
- if (Math.round(X - dx - x - length)) {
- type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
- text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr));
- }
- }
-
- var res = paper.path(path);
-
- res.text = text;
- res.all = paper.set([res, text]);
- res.remove = function () {
- this.text.remove();
- this.constructor.prototype.remove.call(this);
- };
-
- return res;
- },
-
- labelise: function(label, val, total) {
- if (label) {
- return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
- if (value) {
- return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
- }
- if (percent) {
- return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
- }
- });
- } else {
- return (+val).toFixed(0);
- }
- }
-}
diff --git a/vendor/assets/javascripts/jquery.highlight.js b/vendor/assets/javascripts/jquery.highlight.js
deleted file mode 100644
index 7a67cf99844..00000000000
--- a/vendor/assets/javascripts/jquery.highlight.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-
-highlight v3
-
-Highlights arbitrary terms.
-
-<http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html>
-
-MIT license.
-
-Johann Burkard
-<http://johannburkard.de>
-<mailto:jb@eaio.com>
-
-*/
-
-jQuery.fn.highlight = function(pat) {
- function innerHighlight(node, pat) {
- var skip = 0;
- if (node.nodeType == 3) {
- var pos = node.data.toUpperCase().indexOf(pat);
- if (pos >= 0) {
- var spannode = document.createElement('span');
- spannode.className = 'highlight_word';
- var middlebit = node.splitText(pos);
- var endbit = middlebit.splitText(pat.length);
- var middleclone = middlebit.cloneNode(true);
- spannode.appendChild(middleclone);
- middlebit.parentNode.replaceChild(spannode, middlebit);
- skip = 1;
- }
- }
- else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
- for (var i = 0; i < node.childNodes.length; ++i) {
- i += innerHighlight(node.childNodes[i], pat);
- }
- }
- return skip;
- }
- return this.each(function() {
- innerHighlight(this, pat.toUpperCase());
- });
-};
-
-jQuery.fn.removeHighlight = function() {
- return this.find("span.highlight").each(function() {
- this.parentNode.firstChild.nodeName;
- with (this.parentNode) {
- replaceChild(this.firstChild, this);
- normalize();
- }
- }).end();
-};
diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js
deleted file mode 100644
index 3f3f8a0b7f6..00000000000
--- a/vendor/assets/javascripts/raphael.js
+++ /dev/null
@@ -1,8239 +0,0 @@
-// ┌────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\
-// ├────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
-// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\
-// ├────────────────────────────────────────────────────────────────────┤ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\
-// └────────────────────────────────────────────────────────────────────┘ \\
-// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-// ┌────────────────────────────────────────────────────────────┐ \\
-// │ Eve 0.4.2 - JavaScript Events Library │ \\
-// ├────────────────────────────────────────────────────────────┤ \\
-// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\
-// └────────────────────────────────────────────────────────────┘ \\
-
-(function (glob) {
- var version = "0.4.2",
- has = "hasOwnProperty",
- separator = /[\.\/]/,
- wildcard = "*",
- fun = function () {},
- numsort = function (a, b) {
- return a - b;
- },
- current_event,
- stop,
- events = {n: {}},
- /*\
- * eve
- [ method ]
-
- * Fires event with given `name`, given scope and other parameters.
-
- > Arguments
-
- - name (string) name of the *event*, dot (`.`) or slash (`/`) separated
- - scope (object) context for the event handlers
- - varargs (...) the rest of arguments will be sent to event handlers
-
- = (object) array of returned values from the listeners
- \*/
- eve = function (name, scope) {
- name = String(name);
- var e = events,
- oldstop = stop,
- args = Array.prototype.slice.call(arguments, 2),
- listeners = eve.listeners(name),
- z = 0,
- f = false,
- l,
- indexed = [],
- queue = {},
- out = [],
- ce = current_event,
- errors = [];
- current_event = name;
- stop = 0;
- for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) {
- indexed.push(listeners[i].zIndex);
- if (listeners[i].zIndex < 0) {
- queue[listeners[i].zIndex] = listeners[i];
- }
- }
- indexed.sort(numsort);
- while (indexed[z] < 0) {
- l = queue[indexed[z++]];
- out.push(l.apply(scope, args));
- if (stop) {
- stop = oldstop;
- return out;
- }
- }
- for (i = 0; i < ii; i++) {
- l = listeners[i];
- if ("zIndex" in l) {
- if (l.zIndex == indexed[z]) {
- out.push(l.apply(scope, args));
- if (stop) {
- break;
- }
- do {
- z++;
- l = queue[indexed[z]];
- l && out.push(l.apply(scope, args));
- if (stop) {
- break;
- }
- } while (l)
- } else {
- queue[l.zIndex] = l;
- }
- } else {
- out.push(l.apply(scope, args));
- if (stop) {
- break;
- }
- }
- }
- stop = oldstop;
- current_event = ce;
- return out.length ? out : null;
- };
- // Undocumented. Debug only.
- eve._events = events;
- /*\
- * eve.listeners
- [ method ]
-
- * Internal method which gives you array of all event handlers that will be triggered by the given `name`.
-
- > Arguments
-
- - name (string) name of the event, dot (`.`) or slash (`/`) separated
-
- = (array) array of event handlers
- \*/
- eve.listeners = function (name) {
- var names = name.split(separator),
- e = events,
- item,
- items,
- k,
- i,
- ii,
- j,
- jj,
- nes,
- es = [e],
- out = [];
- for (i = 0, ii = names.length; i < ii; i++) {
- nes = [];
- for (j = 0, jj = es.length; j < jj; j++) {
- e = es[j].n;
- items = [e[names[i]], e[wildcard]];
- k = 2;
- while (k--) {
- item = items[k];
- if (item) {
- nes.push(item);
- out = out.concat(item.f || []);
- }
- }
- }
- es = nes;
- }
- return out;
- };
-
- /*\
- * eve.on
- [ method ]
- **
- * Binds given event handler with a given name. You can use wildcards “`*`” for the names:
- | eve.on("*.under.*", f);
- | eve("mouse.under.floor"); // triggers f
- * Use @eve to trigger the listener.
- **
- > Arguments
- **
- - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- - f (function) event handler function
- **
- = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment.
- > Example:
- | eve.on("mouse", eatIt)(2);
- | eve.on("mouse", scream);
- | eve.on("mouse", catchIt)(1);
- * This will ensure that `catchIt()` function will be called before `eatIt()`.
- *
- * If you want to put your handler before non-indexed handlers, specify a negative value.
- * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”.
- \*/
- eve.on = function (name, f) {
- name = String(name);
- if (typeof f != "function") {
- return function () {};
- }
- var names = name.split(separator),
- e = events;
- for (var i = 0, ii = names.length; i < ii; i++) {
- e = e.n;
- e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}});
- }
- e.f = e.f || [];
- for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) {
- return fun;
- }
- e.f.push(f);
- return function (zIndex) {
- if (+zIndex == +zIndex) {
- f.zIndex = +zIndex;
- }
- };
- };
- /*\
- * eve.f
- [ method ]
- **
- * Returns function that will fire given event with optional arguments.
- * Arguments that will be passed to the result function will be also
- * concated to the list of final arguments.
- | el.onclick = eve.f("click", 1, 2);
- | eve.on("click", function (a, b, c) {
- | console.log(a, b, c); // 1, 2, [event object]
- | });
- > Arguments
- - event (string) event name
- - varargs (…) and any other arguments
- = (function) possible event handler function
- \*/
- eve.f = function (event) {
- var attrs = [].slice.call(arguments, 1);
- return function () {
- eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0)));
- };
- };
- /*\
- * eve.stop
- [ method ]
- **
- * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing.
- \*/
- eve.stop = function () {
- stop = 1;
- };
- /*\
- * eve.nt
- [ method ]
- **
- * Could be used inside event handler to figure out actual name of the event.
- **
- > Arguments
- **
- - subname (string) #optional subname of the event
- **
- = (string) name of the event, if `subname` is not specified
- * or
- = (boolean) `true`, if current event’s name contains `subname`
- \*/
- eve.nt = function (subname) {
- if (subname) {
- return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event);
- }
- return current_event;
- };
- /*\
- * eve.nts
- [ method ]
- **
- * Could be used inside event handler to figure out actual name of the event.
- **
- **
- = (array) names of the event
- \*/
- eve.nts = function () {
- return current_event.split(separator);
- };
- /*\
- * eve.off
- [ method ]
- **
- * Removes given function from the list of event listeners assigned to given name.
- * If no arguments specified all the events will be cleared.
- **
- > Arguments
- **
- - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- - f (function) event handler function
- \*/
- /*\
- * eve.unbind
- [ method ]
- **
- * See @eve.off
- \*/
- eve.off = eve.unbind = function (name, f) {
- if (!name) {
- eve._events = events = {n: {}};
- return;
- }
- var names = name.split(separator),
- e,
- key,
- splice,
- i, ii, j, jj,
- cur = [events];
- for (i = 0, ii = names.length; i < ii; i++) {
- for (j = 0; j < cur.length; j += splice.length - 2) {
- splice = [j, 1];
- e = cur[j].n;
- if (names[i] != wildcard) {
- if (e[names[i]]) {
- splice.push(e[names[i]]);
- }
- } else {
- for (key in e) if (e[has](key)) {
- splice.push(e[key]);
- }
- }
- cur.splice.apply(cur, splice);
- }
- }
- for (i = 0, ii = cur.length; i < ii; i++) {
- e = cur[i];
- while (e.n) {
- if (f) {
- if (e.f) {
- for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) {
- e.f.splice(j, 1);
- break;
- }
- !e.f.length && delete e.f;
- }
- for (key in e.n) if (e.n[has](key) && e.n[key].f) {
- var funcs = e.n[key].f;
- for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) {
- funcs.splice(j, 1);
- break;
- }
- !funcs.length && delete e.n[key].f;
- }
- } else {
- delete e.f;
- for (key in e.n) if (e.n[has](key) && e.n[key].f) {
- delete e.n[key].f;
- }
- }
- e = e.n;
- }
- }
- };
- /*\
- * eve.once
- [ method ]
- **
- * Binds given event handler with a given name to only run once then unbind itself.
- | eve.once("login", f);
- | eve("login"); // triggers f
- | eve("login"); // no listeners
- * Use @eve to trigger the listener.
- **
- > Arguments
- **
- - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
- - f (function) event handler function
- **
- = (function) same return function as @eve.on
- \*/
- eve.once = function (name, f) {
- var f2 = function () {
- eve.unbind(name, f2);
- return f.apply(this, arguments);
- };
- return eve.on(name, f2);
- };
- /*\
- * eve.version
- [ property (string) ]
- **
- * Current version of the library.
- \*/
- eve.version = version;
- eve.toString = function () {
- return "You are running Eve " + version;
- };
- (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve));
-})(window || this);
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ "Raphaël 2.1.2" - JavaScript Vector Library │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function (glob, factory) {
- // AMD support
- if (typeof define === "function" && define.amd) {
- // Define as an anonymous module
- define(["eve"], function( eve ) {
- return factory(glob, eve);
- });
- } else {
- // Browser globals (glob is window)
- // Raphael adds itself to window
- factory(glob, glob.eve || (typeof require == "function" && require('eve')) );
- }
-}(this, function (window, eve) {
- /*\
- * Raphael
- [ method ]
- **
- * Creates a canvas object on which to draw.
- * You must do this first, as all future calls to drawing methods
- * from this instance will be bound to this canvas.
- > Parameters
- **
- - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface
- - width (number)
- - height (number)
- - callback (function) #optional callback function which is going to be executed in the context of newly created paper
- * or
- - x (number)
- - y (number)
- - width (number)
- - height (number)
- - callback (function) #optional callback function which is going to be executed in the context of newly created paper
- * or
- - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add.
- - callback (function) #optional callback function which is going to be executed in the context of newly created paper
- * or
- - onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`.
- = (object) @Paper
- > Usage
- | // Each of the following examples create a canvas
- | // that is 320px wide by 200px high.
- | // Canvas is created at the viewport’s 10,50 coordinate.
- | var paper = Raphael(10, 50, 320, 200);
- | // Canvas is created at the top left corner of the #notepad element
- | // (or its top right corner in dir="rtl" elements)
- | var paper = Raphael(document.getElementById("notepad"), 320, 200);
- | // Same as above
- | var paper = Raphael("notepad", 320, 200);
- | // Image dump
- | var set = Raphael(["notepad", 320, 200, {
- | type: "rect",
- | x: 10,
- | y: 10,
- | width: 25,
- | height: 25,
- | stroke: "#f00"
- | }, {
- | type: "text",
- | x: 30,
- | y: 40,
- | text: "Dump"
- | }]);
- \*/
- function R(first) {
- if (R.is(first, "function")) {
- return loaded ? first() : eve.on("raphael.DOMload", first);
- } else if (R.is(first, array)) {
- return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first);
- } else {
- var args = Array.prototype.slice.call(arguments, 0);
- if (R.is(args[args.length - 1], "function")) {
- var f = args.pop();
- return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () {
- f.call(R._engine.create[apply](R, args));
- });
- } else {
- return R._engine.create[apply](R, arguments);
- }
- }
- }
- R.version = "2.1.2";
- R.eve = eve;
- var loaded,
- separator = /[, ]+/,
- elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1},
- formatrg = /\{(\d+)\}/g,
- proto = "prototype",
- has = "hasOwnProperty",
- g = {
- doc: document,
- win: window
- },
- oldRaphael = {
- was: Object.prototype[has].call(g.win, "Raphael"),
- is: g.win.Raphael
- },
- Paper = function () {
- /*\
- * Paper.ca
- [ property (object) ]
- **
- * Shortcut for @Paper.customAttributes
- \*/
- /*\
- * Paper.customAttributes
- [ property (object) ]
- **
- * If you have a set of attributes that you would like to represent
- * as a function of some number you can do it easily with custom attributes:
- > Usage
- | paper.customAttributes.hue = function (num) {
- | num = num % 1;
- | return {fill: "hsb(" + num + ", 0.75, 1)"};
- | };
- | // Custom attribute “hue” will change fill
- | // to be given hue with fixed saturation and brightness.
- | // Now you can use it like this:
- | var c = paper.circle(10, 10, 10).attr({hue: .45});
- | // or even like this:
- | c.animate({hue: 1}, 1e3);
- |
- | // You could also create custom attribute
- | // with multiple parameters:
- | paper.customAttributes.hsb = function (h, s, b) {
- | return {fill: "hsb(" + [h, s, b].join(",") + ")"};
- | };
- | c.attr({hsb: "0.5 .8 1"});
- | c.animate({hsb: [1, 0, 0.5]}, 1e3);
- \*/
- this.ca = this.customAttributes = {};
- },
- paperproto,
- appendChild = "appendChild",
- apply = "apply",
- concat = "concat",
- supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test
- E = "",
- S = " ",
- Str = String,
- split = "split",
- events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S),
- touchMap = {
- mousedown: "touchstart",
- mousemove: "touchmove",
- mouseup: "touchend"
- },
- lowerCase = Str.prototype.toLowerCase,
- math = Math,
- mmax = math.max,
- mmin = math.min,
- abs = math.abs,
- pow = math.pow,
- PI = math.PI,
- nu = "number",
- string = "string",
- array = "array",
- toString = "toString",
- fillString = "fill",
- objectToString = Object.prototype.toString,
- paper = {},
- push = "push",
- ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i,
- colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i,
- isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1},
- bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
- round = math.round,
- setAttribute = "setAttribute",
- toFloat = parseFloat,
- toInt = parseInt,
- upperCase = Str.prototype.toUpperCase,
- availableAttrs = R._availableAttrs = {
- "arrow-end": "none",
- "arrow-start": "none",
- blur: 0,
- "clip-rect": "0 0 1e9 1e9",
- cursor: "default",
- cx: 0,
- cy: 0,
- fill: "#fff",
- "fill-opacity": 1,
- font: '10px "Arial"',
- "font-family": '"Arial"',
- "font-size": "10",
- "font-style": "normal",
- "font-weight": 400,
- gradient: 0,
- height: 0,
- href: "http://raphaeljs.com/",
- "letter-spacing": 0,
- opacity: 1,
- path: "M0,0",
- r: 0,
- rx: 0,
- ry: 0,
- src: "",
- stroke: "#000",
- "stroke-dasharray": "",
- "stroke-linecap": "butt",
- "stroke-linejoin": "butt",
- "stroke-miterlimit": 0,
- "stroke-opacity": 1,
- "stroke-width": 1,
- target: "_blank",
- "text-anchor": "middle",
- title: "Raphael",
- transform: "",
- width: 0,
- x: 0,
- y: 0
- },
- availableAnimAttrs = R._availableAnimAttrs = {
- blur: nu,
- "clip-rect": "csv",
- cx: nu,
- cy: nu,
- fill: "colour",
- "fill-opacity": nu,
- "font-size": nu,
- height: nu,
- opacity: nu,
- path: "path",
- r: nu,
- rx: nu,
- ry: nu,
- stroke: "colour",
- "stroke-opacity": nu,
- "stroke-width": nu,
- transform: "transform",
- width: nu,
- x: nu,
- y: nu
- },
- whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g,
- commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/,
- hsrg = {hs: 1, rg: 1},
- p2s = /,?([achlmqrstvxz]),?/gi,
- pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
- tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
- pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig,
- radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/,
- eldata = {},
- sortByKey = function (a, b) {
- return a.key - b.key;
- },
- sortByNumber = function (a, b) {
- return toFloat(a) - toFloat(b);
- },
- fun = function () {},
- pipe = function (x) {
- return x;
- },
- rectPath = R._rectPath = function (x, y, w, h, r) {
- if (r) {
- return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
- }
- return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
- },
- ellipsePath = function (x, y, rx, ry) {
- if (ry == null) {
- ry = rx;
- }
- return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
- },
- getPath = R._getPath = {
- path: function (el) {
- return el.attr("path");
- },
- circle: function (el) {
- var a = el.attrs;
- return ellipsePath(a.cx, a.cy, a.r);
- },
- ellipse: function (el) {
- var a = el.attrs;
- return ellipsePath(a.cx, a.cy, a.rx, a.ry);
- },
- rect: function (el) {
- var a = el.attrs;
- return rectPath(a.x, a.y, a.width, a.height, a.r);
- },
- image: function (el) {
- var a = el.attrs;
- return rectPath(a.x, a.y, a.width, a.height);
- },
- text: function (el) {
- var bbox = el._getBBox();
- return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
- },
- set : function(el) {
- var bbox = el._getBBox();
- return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
- }
- },
- /*\
- * Raphael.mapPath
- [ method ]
- **
- * Transform the path string with given matrix.
- > Parameters
- - path (string) path string
- - matrix (object) see @Matrix
- = (string) transformed path string
- \*/
- mapPath = R.mapPath = function (path, matrix) {
- if (!matrix) {
- return path;
- }
- var x, y, i, j, ii, jj, pathi;
- path = path2curve(path);
- for (i = 0, ii = path.length; i < ii; i++) {
- pathi = path[i];
- for (j = 1, jj = pathi.length; j < jj; j += 2) {
- x = matrix.x(pathi[j], pathi[j + 1]);
- y = matrix.y(pathi[j], pathi[j + 1]);
- pathi[j] = x;
- pathi[j + 1] = y;
- }
- }
- return path;
- };
-
- R._g = g;
- /*\
- * Raphael.type
- [ property (string) ]
- **
- * Can be “SVG”, “VML” or empty, depending on browser support.
- \*/
- R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
- if (R.type == "VML") {
- var d = g.doc.createElement("div"),
- b;
- d.innerHTML = '<v:shape adj="1"/>';
- b = d.firstChild;
- b.style.behavior = "url(#default#VML)";
- if (!(b && typeof b.adj == "object")) {
- return (R.type = E);
- }
- d = null;
- }
- /*\
- * Raphael.svg
- [ property (boolean) ]
- **
- * `true` if browser supports SVG.
- \*/
- /*\
- * Raphael.vml
- [ property (boolean) ]
- **
- * `true` if browser supports VML.
- \*/
- R.svg = !(R.vml = R.type == "VML");
- R._Paper = Paper;
- /*\
- * Raphael.fn
- [ property (object) ]
- **
- * You can add your own method to the canvas. For example if you want to draw a pie chart,
- * you can create your own pie chart function and ship it as a Raphaël plugin. To do this
- * you need to extend the `Raphael.fn` object. You should modify the `fn` object before a
- * Raphaël instance is created, otherwise it will take no effect. Please note that the
- * ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to
- * ensure any namespacing ensures proper context.
- > Usage
- | Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
- | return this.path( ... );
- | };
- | // or create namespace
- | Raphael.fn.mystuff = {
- | arrow: function () {…},
- | star: function () {…},
- | // etc…
- | };
- | var paper = Raphael(10, 10, 630, 480);
- | // then use it
- | paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"});
- | paper.mystuff.arrow();
- | paper.mystuff.star();
- \*/
- R.fn = paperproto = Paper.prototype = R.prototype;
- R._id = 0;
- R._oid = 0;
- /*\
- * Raphael.is
- [ method ]
- **
- * Handful of replacements for `typeof` operator.
- > Parameters
- - o (…) any object or primitive
- - type (string) name of the type, i.e. “string”, “function”, “number”, etc.
- = (boolean) is given value is of given type
- \*/
- R.is = function (o, type) {
- type = lowerCase.call(type);
- if (type == "finite") {
- return !isnan[has](+o);
- }
- if (type == "array") {
- return o instanceof Array;
- }
- return (type == "null" && o === null) ||
- (type == typeof o && o !== null) ||
- (type == "object" && o === Object(o)) ||
- (type == "array" && Array.isArray && Array.isArray(o)) ||
- objectToString.call(o).slice(8, -1).toLowerCase() == type;
- };
-
- function clone(obj) {
- if (typeof obj == "function" || Object(obj) !== obj) {
- return obj;
- }
- var res = new obj.constructor;
- for (var key in obj) if (obj[has](key)) {
- res[key] = clone(obj[key]);
- }
- return res;
- }
-
- /*\
- * Raphael.angle
- [ method ]
- **
- * Returns angle between two or three points
- > Parameters
- - x1 (number) x coord of first point
- - y1 (number) y coord of first point
- - x2 (number) x coord of second point
- - y2 (number) y coord of second point
- - x3 (number) #optional x coord of third point
- - y3 (number) #optional y coord of third point
- = (number) angle in degrees.
- \*/
- R.angle = function (x1, y1, x2, y2, x3, y3) {
- if (x3 == null) {
- var x = x1 - x2,
- y = y1 - y2;
- if (!x && !y) {
- return 0;
- }
- return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360;
- } else {
- return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3);
- }
- };
- /*\
- * Raphael.rad
- [ method ]
- **
- * Transform angle to radians
- > Parameters
- - deg (number) angle in degrees
- = (number) angle in radians.
- \*/
- R.rad = function (deg) {
- return deg % 360 * PI / 180;
- };
- /*\
- * Raphael.deg
- [ method ]
- **
- * Transform angle to degrees
- > Parameters
- - rad (number) angle in radians
- = (number) angle in degrees.
- \*/
- R.deg = function (rad) {
- return Math.round ((rad * 180 / PI% 360)* 1000) / 1000;
- };
- /*\
- * Raphael.snapTo
- [ method ]
- **
- * Snaps given value to given grid.
- > Parameters
- - values (array|number) given array of values or step of the grid
- - value (number) value to adjust
- - tolerance (number) #optional tolerance for snapping. Default is `10`.
- = (number) adjusted value.
- \*/
- R.snapTo = function (values, value, tolerance) {
- tolerance = R.is(tolerance, "finite") ? tolerance : 10;
- if (R.is(values, array)) {
- var i = values.length;
- while (i--) if (abs(values[i] - value) <= tolerance) {
- return values[i];
- }
- } else {
- values = +values;
- var rem = value % values;
- if (rem < tolerance) {
- return value - rem;
- }
- if (rem > values - tolerance) {
- return value - rem + values;
- }
- }
- return value;
- };
-
- /*\
- * Raphael.createUUID
- [ method ]
- **
- * Returns RFC4122, version 4 ID
- \*/
- var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) {
- return function () {
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase();
- };
- })(/[xy]/g, function (c) {
- var r = math.random() * 16 | 0,
- v = c == "x" ? r : (r & 3 | 8);
- return v.toString(16);
- });
-
- /*\
- * Raphael.setWindow
- [ method ]
- **
- * Used when you need to draw in `&lt;iframe>`. Switched window to the iframe one.
- > Parameters
- - newwin (window) new window object
- \*/
- R.setWindow = function (newwin) {
- eve("raphael.setWindow", R, g.win, newwin);
- g.win = newwin;
- g.doc = g.win.document;
- if (R._engine.initWin) {
- R._engine.initWin(g.win);
- }
- };
- var toHex = function (color) {
- if (R.vml) {
- // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/
- var trim = /^\s+|\s+$/g;
- var bod;
- try {
- var docum = new ActiveXObject("htmlfile");
- docum.write("<body>");
- docum.close();
- bod = docum.body;
- } catch(e) {
- bod = createPopup().document.body;
- }
- var range = bod.createTextRange();
- toHex = cacher(function (color) {
- try {
- bod.style.color = Str(color).replace(trim, E);
- var value = range.queryCommandValue("ForeColor");
- value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16);
- return "#" + ("000000" + value.toString(16)).slice(-6);
- } catch(e) {
- return "none";
- }
- });
- } else {
- var i = g.doc.createElement("i");
- i.title = "Rapha\xebl Colour Picker";
- i.style.display = "none";
- g.doc.body.appendChild(i);
- toHex = cacher(function (color) {
- i.style.color = color;
- return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color");
- });
- }
- return toHex(color);
- },
- hsbtoString = function () {
- return "hsb(" + [this.h, this.s, this.b] + ")";
- },
- hsltoString = function () {
- return "hsl(" + [this.h, this.s, this.l] + ")";
- },
- rgbtoString = function () {
- return this.hex;
- },
- prepareRGB = function (r, g, b) {
- if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) {
- b = r.b;
- g = r.g;
- r = r.r;
- }
- if (g == null && R.is(r, string)) {
- var clr = R.getRGB(r);
- r = clr.r;
- g = clr.g;
- b = clr.b;
- }
- if (r > 1 || g > 1 || b > 1) {
- r /= 255;
- g /= 255;
- b /= 255;
- }
-
- return [r, g, b];
- },
- packageRGB = function (r, g, b, o) {
- r *= 255;
- g *= 255;
- b *= 255;
- var rgb = {
- r: r,
- g: g,
- b: b,
- hex: R.rgb(r, g, b),
- toString: rgbtoString
- };
- R.is(o, "finite") && (rgb.opacity = o);
- return rgb;
- };
-
- /*\
- * Raphael.color
- [ method ]
- **
- * Parses the color string and returns object with all values for the given color.
- > Parameters
- - clr (string) color string in one of the supported formats (see @Raphael.getRGB)
- = (object) Combined RGB & HSB object in format:
- o {
- o r (number) red,
- o g (number) green,
- o b (number) blue,
- o hex (string) color in HTML/CSS format: #••••••,
- o error (boolean) `true` if string can’t be parsed,
- o h (number) hue,
- o s (number) saturation,
- o v (number) value (brightness),
- o l (number) lightness
- o }
- \*/
- R.color = function (clr) {
- var rgb;
- if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) {
- rgb = R.hsb2rgb(clr);
- clr.r = rgb.r;
- clr.g = rgb.g;
- clr.b = rgb.b;
- clr.hex = rgb.hex;
- } else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) {
- rgb = R.hsl2rgb(clr);
- clr.r = rgb.r;
- clr.g = rgb.g;
- clr.b = rgb.b;
- clr.hex = rgb.hex;
- } else {
- if (R.is(clr, "string")) {
- clr = R.getRGB(clr);
- }
- if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) {
- rgb = R.rgb2hsl(clr);
- clr.h = rgb.h;
- clr.s = rgb.s;
- clr.l = rgb.l;
- rgb = R.rgb2hsb(clr);
- clr.v = rgb.b;
- } else {
- clr = {hex: "none"};
- clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1;
- }
- }
- clr.toString = rgbtoString;
- return clr;
- };
- /*\
- * Raphael.hsb2rgb
- [ method ]
- **
- * Converts HSB values to RGB object.
- > Parameters
- - h (number) hue
- - s (number) saturation
- - v (number) value or brightness
- = (object) RGB object in format:
- o {
- o r (number) red,
- o g (number) green,
- o b (number) blue,
- o hex (string) color in HTML/CSS format: #••••••
- o }
- \*/
- R.hsb2rgb = function (h, s, v, o) {
- if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) {
- v = h.b;
- s = h.s;
- o = h.o;
- h = h.h;
- }
- h *= 360;
- var R, G, B, X, C;
- h = (h % 360) / 60;
- C = v * s;
- X = C * (1 - abs(h % 2 - 1));
- R = G = B = v - C;
-
- h = ~~h;
- R += [C, X, 0, 0, X, C][h];
- G += [X, C, C, X, 0, 0][h];
- B += [0, 0, X, C, C, X][h];
- return packageRGB(R, G, B, o);
- };
- /*\
- * Raphael.hsl2rgb
- [ method ]
- **
- * Converts HSL values to RGB object.
- > Parameters
- - h (number) hue
- - s (number) saturation
- - l (number) luminosity
- = (object) RGB object in format:
- o {
- o r (number) red,
- o g (number) green,
- o b (number) blue,
- o hex (string) color in HTML/CSS format: #••••••
- o }
- \*/
- R.hsl2rgb = function (h, s, l, o) {
- if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) {
- l = h.l;
- s = h.s;
- h = h.h;
- }
- if (h > 1 || s > 1 || l > 1) {
- h /= 360;
- s /= 100;
- l /= 100;
- }
- h *= 360;
- var R, G, B, X, C;
- h = (h % 360) / 60;
- C = 2 * s * (l < .5 ? l : 1 - l);
- X = C * (1 - abs(h % 2 - 1));
- R = G = B = l - C / 2;
-
- h = ~~h;
- R += [C, X, 0, 0, X, C][h];
- G += [X, C, C, X, 0, 0][h];
- B += [0, 0, X, C, C, X][h];
- return packageRGB(R, G, B, o);
- };
- /*\
- * Raphael.rgb2hsb
- [ method ]
- **
- * Converts RGB values to HSB object.
- > Parameters
- - r (number) red
- - g (number) green
- - b (number) blue
- = (object) HSB object in format:
- o {
- o h (number) hue
- o s (number) saturation
- o b (number) brightness
- o }
- \*/
- R.rgb2hsb = function (r, g, b) {
- b = prepareRGB(r, g, b);
- r = b[0];
- g = b[1];
- b = b[2];
-
- var H, S, V, C;
- V = mmax(r, g, b);
- C = V - mmin(r, g, b);
- H = (C == 0 ? null :
- V == r ? (g - b) / C :
- V == g ? (b - r) / C + 2 :
- (r - g) / C + 4
- );
- H = ((H + 360) % 6) * 60 / 360;
- S = C == 0 ? 0 : C / V;
- return {h: H, s: S, b: V, toString: hsbtoString};
- };
- /*\
- * Raphael.rgb2hsl
- [ method ]
- **
- * Converts RGB values to HSL object.
- > Parameters
- - r (number) red
- - g (number) green
- - b (number) blue
- = (object) HSL object in format:
- o {
- o h (number) hue
- o s (number) saturation
- o l (number) luminosity
- o }
- \*/
- R.rgb2hsl = function (r, g, b) {
- b = prepareRGB(r, g, b);
- r = b[0];
- g = b[1];
- b = b[2];
-
- var H, S, L, M, m, C;
- M = mmax(r, g, b);
- m = mmin(r, g, b);
- C = M - m;
- H = (C == 0 ? null :
- M == r ? (g - b) / C :
- M == g ? (b - r) / C + 2 :
- (r - g) / C + 4);
- H = ((H + 360) % 6) * 60 / 360;
- L = (M + m) / 2;
- S = (C == 0 ? 0 :
- L < .5 ? C / (2 * L) :
- C / (2 - 2 * L));
- return {h: H, s: S, l: L, toString: hsltoString};
- };
- R._path2string = function () {
- return this.join(",").replace(p2s, "$1");
- };
- function repush(array, item) {
- for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) {
- return array.push(array.splice(i, 1)[0]);
- }
- }
- function cacher(f, scope, postprocessor) {
- function newf() {
- var arg = Array.prototype.slice.call(arguments, 0),
- args = arg.join("\u2400"),
- cache = newf.cache = newf.cache || {},
- count = newf.count = newf.count || [];
- if (cache[has](args)) {
- repush(count, args);
- return postprocessor ? postprocessor(cache[args]) : cache[args];
- }
- count.length >= 1e3 && delete cache[count.shift()];
- count.push(args);
- cache[args] = f[apply](scope, arg);
- return postprocessor ? postprocessor(cache[args]) : cache[args];
- }
- return newf;
- }
-
- var preload = R._preload = function (src, f) {
- var img = g.doc.createElement("img");
- img.style.cssText = "position:absolute;left:-9999em;top:-9999em";
- img.onload = function () {
- f.call(this);
- this.onload = null;
- g.doc.body.removeChild(this);
- };
- img.onerror = function () {
- g.doc.body.removeChild(this);
- };
- g.doc.body.appendChild(img);
- img.src = src;
- };
-
- function clrToString() {
- return this.hex;
- }
-
- /*\
- * Raphael.getRGB
- [ method ]
- **
- * Parses colour string as RGB object
- > Parameters
- - colour (string) colour string in one of formats:
- # <ul>
- # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
- # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
- # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
- # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
- # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
- # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
- # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
- # <li>hsl(•••, •••, •••) — same as hsb</li>
- # <li>hsl(•••%, •••%, •••%) — same as hsb</li>
- # </ul>
- = (object) RGB object in format:
- o {
- o r (number) red,
- o g (number) green,
- o b (number) blue
- o hex (string) color in HTML/CSS format: #••••••,
- o error (boolean) true if string can’t be parsed
- o }
- \*/
- R.getRGB = cacher(function (colour) {
- if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) {
- return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
- }
- if (colour == "none") {
- return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString};
- }
- !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour));
- var res,
- red,
- green,
- blue,
- opacity,
- t,
- values,
- rgb = colour.match(colourRegExp);
- if (rgb) {
- if (rgb[2]) {
- blue = toInt(rgb[2].substring(5), 16);
- green = toInt(rgb[2].substring(3, 5), 16);
- red = toInt(rgb[2].substring(1, 3), 16);
- }
- if (rgb[3]) {
- blue = toInt((t = rgb[3].charAt(3)) + t, 16);
- green = toInt((t = rgb[3].charAt(2)) + t, 16);
- red = toInt((t = rgb[3].charAt(1)) + t, 16);
- }
- if (rgb[4]) {
- values = rgb[4][split](commaSpaces);
- red = toFloat(values[0]);
- values[0].slice(-1) == "%" && (red *= 2.55);
- green = toFloat(values[1]);
- values[1].slice(-1) == "%" && (green *= 2.55);
- blue = toFloat(values[2]);
- values[2].slice(-1) == "%" && (blue *= 2.55);
- rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3]));
- values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
- }
- if (rgb[5]) {
- values = rgb[5][split](commaSpaces);
- red = toFloat(values[0]);
- values[0].slice(-1) == "%" && (red *= 2.55);
- green = toFloat(values[1]);
- values[1].slice(-1) == "%" && (green *= 2.55);
- blue = toFloat(values[2]);
- values[2].slice(-1) == "%" && (blue *= 2.55);
- (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
- rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3]));
- values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
- return R.hsb2rgb(red, green, blue, opacity);
- }
- if (rgb[6]) {
- values = rgb[6][split](commaSpaces);
- red = toFloat(values[0]);
- values[0].slice(-1) == "%" && (red *= 2.55);
- green = toFloat(values[1]);
- values[1].slice(-1) == "%" && (green *= 2.55);
- blue = toFloat(values[2]);
- values[2].slice(-1) == "%" && (blue *= 2.55);
- (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
- rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3]));
- values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
- return R.hsl2rgb(red, green, blue, opacity);
- }
- rgb = {r: red, g: green, b: blue, toString: clrToString};
- rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1);
- R.is(opacity, "finite") && (rgb.opacity = opacity);
- return rgb;
- }
- return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
- }, R);
- /*\
- * Raphael.hsb
- [ method ]
- **
- * Converts HSB values to hex representation of the colour.
- > Parameters
- - h (number) hue
- - s (number) saturation
- - b (number) value or brightness
- = (string) hex representation of the colour.
- \*/
- R.hsb = cacher(function (h, s, b) {
- return R.hsb2rgb(h, s, b).hex;
- });
- /*\
- * Raphael.hsl
- [ method ]
- **
- * Converts HSL values to hex representation of the colour.
- > Parameters
- - h (number) hue
- - s (number) saturation
- - l (number) luminosity
- = (string) hex representation of the colour.
- \*/
- R.hsl = cacher(function (h, s, l) {
- return R.hsl2rgb(h, s, l).hex;
- });
- /*\
- * Raphael.rgb
- [ method ]
- **
- * Converts RGB values to hex representation of the colour.
- > Parameters
- - r (number) red
- - g (number) green
- - b (number) blue
- = (string) hex representation of the colour.
- \*/
- R.rgb = cacher(function (r, g, b) {
- return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1);
- });
- /*\
- * Raphael.getColor
- [ method ]
- **
- * On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset
- > Parameters
- - value (number) #optional brightness, default is `0.75`
- = (string) hex representation of the colour.
- \*/
- R.getColor = function (value) {
- var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75},
- rgb = this.hsb2rgb(start.h, start.s, start.b);
- start.h += .075;
- if (start.h > 1) {
- start.h = 0;
- start.s -= .2;
- start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b});
- }
- return rgb.hex;
- };
- /*\
- * Raphael.getColor.reset
- [ method ]
- **
- * Resets spectrum position for @Raphael.getColor back to red.
- \*/
- R.getColor.reset = function () {
- delete this.start;
- };
-
- // http://schepers.cc/getting-to-the-point
- function catmullRom2bezier(crp, z) {
- var d = [];
- for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) {
- var p = [
- {x: +crp[i - 2], y: +crp[i - 1]},
- {x: +crp[i], y: +crp[i + 1]},
- {x: +crp[i + 2], y: +crp[i + 3]},
- {x: +crp[i + 4], y: +crp[i + 5]}
- ];
- if (z) {
- if (!i) {
- p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]};
- } else if (iLen - 4 == i) {
- p[3] = {x: +crp[0], y: +crp[1]};
- } else if (iLen - 2 == i) {
- p[2] = {x: +crp[0], y: +crp[1]};
- p[3] = {x: +crp[2], y: +crp[3]};
- }
- } else {
- if (iLen - 4 == i) {
- p[3] = p[2];
- } else if (!i) {
- p[0] = {x: +crp[i], y: +crp[i + 1]};
- }
- }
- d.push(["C",
- (-p[0].x + 6 * p[1].x + p[2].x) / 6,
- (-p[0].y + 6 * p[1].y + p[2].y) / 6,
- (p[1].x + 6 * p[2].x - p[3].x) / 6,
- (p[1].y + 6*p[2].y - p[3].y) / 6,
- p[2].x,
- p[2].y
- ]);
- }
-
- return d;
- }
- /*\
- * Raphael.parsePathString
- [ method ]
- **
- * Utility method
- **
- * Parses given path string into an array of arrays of path segments.
- > Parameters
- - pathString (string|array) path string or array of segments (in the last case it will be returned straight away)
- = (array) array of segments.
- \*/
- R.parsePathString = function (pathString) {
- if (!pathString) {
- return null;
- }
- var pth = paths(pathString);
- if (pth.arr) {
- return pathClone(pth.arr);
- }
-
- var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0},
- data = [];
- if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption
- data = pathClone(pathString);
- }
- if (!data.length) {
- Str(pathString).replace(pathCommand, function (a, b, c) {
- var params = [],
- name = b.toLowerCase();
- c.replace(pathValues, function (a, b) {
- b && params.push(+b);
- });
- if (name == "m" && params.length > 2) {
- data.push([b][concat](params.splice(0, 2)));
- name = "l";
- b = b == "m" ? "l" : "L";
- }
- if (name == "r") {
- data.push([b][concat](params));
- } else while (params.length >= paramCounts[name]) {
- data.push([b][concat](params.splice(0, paramCounts[name])));
- if (!paramCounts[name]) {
- break;
- }
- }
- });
- }
- data.toString = R._path2string;
- pth.arr = pathClone(data);
- return data;
- };
- /*\
- * Raphael.parseTransformString
- [ method ]
- **
- * Utility method
- **
- * Parses given path string into an array of transformations.
- > Parameters
- - TString (string|array) transform string or array of transformations (in the last case it will be returned straight away)
- = (array) array of transformations.
- \*/
- R.parseTransformString = cacher(function (TString) {
- if (!TString) {
- return null;
- }
- var paramCounts = {r: 3, s: 4, t: 2, m: 6},
- data = [];
- if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption
- data = pathClone(TString);
- }
- if (!data.length) {
- Str(TString).replace(tCommand, function (a, b, c) {
- var params = [],
- name = lowerCase.call(b);
- c.replace(pathValues, function (a, b) {
- b && params.push(+b);
- });
- data.push([b][concat](params));
- });
- }
- data.toString = R._path2string;
- return data;
- });
- // PATHS
- var paths = function (ps) {
- var p = paths.ps = paths.ps || {};
- if (p[ps]) {
- p[ps].sleep = 100;
- } else {
- p[ps] = {
- sleep: 100
- };
- }
- setTimeout(function () {
- for (var key in p) if (p[has](key) && key != ps) {
- p[key].sleep--;
- !p[key].sleep && delete p[key];
- }
- });
- return p[ps];
- };
- /*\
- * Raphael.findDotsAtSegment
- [ method ]
- **
- * Utility method
- **
- * Find dot coordinates on the given cubic bezier curve at the given t.
- > Parameters
- - p1x (number) x of the first point of the curve
- - p1y (number) y of the first point of the curve
- - c1x (number) x of the first anchor of the curve
- - c1y (number) y of the first anchor of the curve
- - c2x (number) x of the second anchor of the curve
- - c2y (number) y of the second anchor of the curve
- - p2x (number) x of the second point of the curve
- - p2y (number) y of the second point of the curve
- - t (number) position on the curve (0..1)
- = (object) point information in format:
- o {
- o x: (number) x coordinate of the point
- o y: (number) y coordinate of the point
- o m: {
- o x: (number) x coordinate of the left anchor
- o y: (number) y coordinate of the left anchor
- o }
- o n: {
- o x: (number) x coordinate of the right anchor
- o y: (number) y coordinate of the right anchor
- o }
- o start: {
- o x: (number) x coordinate of the start of the curve
- o y: (number) y coordinate of the start of the curve
- o }
- o end: {
- o x: (number) x coordinate of the end of the curve
- o y: (number) y coordinate of the end of the curve
- o }
- o alpha: (number) angle of the curve derivative at the point
- o }
- \*/
- R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
- var t1 = 1 - t,
- t13 = pow(t1, 3),
- t12 = pow(t1, 2),
- t2 = t * t,
- t3 = t2 * t,
- x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x,
- y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y,
- mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x),
- my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y),
- nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x),
- ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y),
- ax = t1 * p1x + t * c1x,
- ay = t1 * p1y + t * c1y,
- cx = t1 * c2x + t * p2x,
- cy = t1 * c2y + t * p2y,
- alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI);
- (mx > nx || my < ny) && (alpha += 180);
- return {
- x: x,
- y: y,
- m: {x: mx, y: my},
- n: {x: nx, y: ny},
- start: {x: ax, y: ay},
- end: {x: cx, y: cy},
- alpha: alpha
- };
- };
- /*\
- * Raphael.bezierBBox
- [ method ]
- **
- * Utility method
- **
- * Return bounding box of a given cubic bezier curve
- > Parameters
- - p1x (number) x of the first point of the curve
- - p1y (number) y of the first point of the curve
- - c1x (number) x of the first anchor of the curve
- - c1y (number) y of the first anchor of the curve
- - c2x (number) x of the second anchor of the curve
- - c2y (number) y of the second anchor of the curve
- - p2x (number) x of the second point of the curve
- - p2y (number) y of the second point of the curve
- * or
- - bez (array) array of six points for bezier curve
- = (object) point information in format:
- o {
- o min: {
- o x: (number) x coordinate of the left point
- o y: (number) y coordinate of the top point
- o }
- o max: {
- o x: (number) x coordinate of the right point
- o y: (number) y coordinate of the bottom point
- o }
- o }
- \*/
- R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
- if (!R.is(p1x, "array")) {
- p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y];
- }
- var bbox = curveDim.apply(null, p1x);
- return {
- x: bbox.min.x,
- y: bbox.min.y,
- x2: bbox.max.x,
- y2: bbox.max.y,
- width: bbox.max.x - bbox.min.x,
- height: bbox.max.y - bbox.min.y
- };
- };
- /*\
- * Raphael.isPointInsideBBox
- [ method ]
- **
- * Utility method
- **
- * Returns `true` if given point is inside bounding boxes.
- > Parameters
- - bbox (string) bounding box
- - x (string) x coordinate of the point
- - y (string) y coordinate of the point
- = (boolean) `true` if point inside
- \*/
- R.isPointInsideBBox = function (bbox, x, y) {
- return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2;
- };
- /*\
- * Raphael.isBBoxIntersect
- [ method ]
- **
- * Utility method
- **
- * Returns `true` if two bounding boxes intersect
- > Parameters
- - bbox1 (string) first bounding box
- - bbox2 (string) second bounding box
- = (boolean) `true` if they intersect
- \*/
- R.isBBoxIntersect = function (bbox1, bbox2) {
- var i = R.isPointInsideBBox;
- return i(bbox2, bbox1.x, bbox1.y)
- || i(bbox2, bbox1.x2, bbox1.y)
- || i(bbox2, bbox1.x, bbox1.y2)
- || i(bbox2, bbox1.x2, bbox1.y2)
- || i(bbox1, bbox2.x, bbox2.y)
- || i(bbox1, bbox2.x2, bbox2.y)
- || i(bbox1, bbox2.x, bbox2.y2)
- || i(bbox1, bbox2.x2, bbox2.y2)
- || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x)
- && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y);
- };
- function base3(t, p1, p2, p3, p4) {
- var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
- t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
- return t * t2 - 3 * p1 + 3 * p2;
- }
- function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) {
- if (z == null) {
- z = 1;
- }
- z = z > 1 ? 1 : z < 0 ? 0 : z;
- var z2 = z / 2,
- n = 12,
- Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816],
- Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472],
- sum = 0;
- for (var i = 0; i < n; i++) {
- var ct = z2 * Tvalues[i] + z2,
- xbase = base3(ct, x1, x2, x3, x4),
- ybase = base3(ct, y1, y2, y3, y4),
- comb = xbase * xbase + ybase * ybase;
- sum += Cvalues[i] * math.sqrt(comb);
- }
- return z2 * sum;
- }
- function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) {
- if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) {
- return;
- }
- var t = 1,
- step = t / 2,
- t2 = t - step,
- l,
- e = .01;
- l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
- while (abs(l - ll) > e) {
- step /= 2;
- t2 += (l < ll ? 1 : -1) * step;
- l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
- }
- return t2;
- }
- function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
- if (
- mmax(x1, x2) < mmin(x3, x4) ||
- mmin(x1, x2) > mmax(x3, x4) ||
- mmax(y1, y2) < mmin(y3, y4) ||
- mmin(y1, y2) > mmax(y3, y4)
- ) {
- return;
- }
- var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
- ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
- denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
-
- if (!denominator) {
- return;
- }
- var px = nx / denominator,
- py = ny / denominator,
- px2 = +px.toFixed(2),
- py2 = +py.toFixed(2);
- if (
- px2 < +mmin(x1, x2).toFixed(2) ||
- px2 > +mmax(x1, x2).toFixed(2) ||
- px2 < +mmin(x3, x4).toFixed(2) ||
- px2 > +mmax(x3, x4).toFixed(2) ||
- py2 < +mmin(y1, y2).toFixed(2) ||
- py2 > +mmax(y1, y2).toFixed(2) ||
- py2 < +mmin(y3, y4).toFixed(2) ||
- py2 > +mmax(y3, y4).toFixed(2)
- ) {
- return;
- }
- return {x: px, y: py};
- }
- function inter(bez1, bez2) {
- return interHelper(bez1, bez2);
- }
- function interCount(bez1, bez2) {
- return interHelper(bez1, bez2, 1);
- }
- function interHelper(bez1, bez2, justCount) {
- var bbox1 = R.bezierBBox(bez1),
- bbox2 = R.bezierBBox(bez2);
- if (!R.isBBoxIntersect(bbox1, bbox2)) {
- return justCount ? 0 : [];
- }
- var l1 = bezlen.apply(0, bez1),
- l2 = bezlen.apply(0, bez2),
- n1 = mmax(~~(l1 / 5), 1),
- n2 = mmax(~~(l2 / 5), 1),
- dots1 = [],
- dots2 = [],
- xy = {},
- res = justCount ? 0 : [];
- for (var i = 0; i < n1 + 1; i++) {
- var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1));
- dots1.push({x: p.x, y: p.y, t: i / n1});
- }
- for (i = 0; i < n2 + 1; i++) {
- p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2));
- dots2.push({x: p.x, y: p.y, t: i / n2});
- }
- for (i = 0; i < n1; i++) {
- for (var j = 0; j < n2; j++) {
- var di = dots1[i],
- di1 = dots1[i + 1],
- dj = dots2[j],
- dj1 = dots2[j + 1],
- ci = abs(di1.x - di.x) < .001 ? "y" : "x",
- cj = abs(dj1.x - dj.x) < .001 ? "y" : "x",
- is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
- if (is) {
- if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) {
- continue;
- }
- xy[is.x.toFixed(4)] = is.y.toFixed(4);
- var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t),
- t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
- if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) {
- if (justCount) {
- res++;
- } else {
- res.push({
- x: is.x,
- y: is.y,
- t1: mmin(t1, 1),
- t2: mmin(t2, 1)
- });
- }
- }
- }
- }
- }
- return res;
- }
- /*\
- * Raphael.pathIntersection
- [ method ]
- **
- * Utility method
- **
- * Finds intersections of two paths
- > Parameters
- - path1 (string) path string
- - path2 (string) path string
- = (array) dots of intersection
- o [
- o {
- o x: (number) x coordinate of the point
- o y: (number) y coordinate of the point
- o t1: (number) t value for segment of path1
- o t2: (number) t value for segment of path2
- o segment1: (number) order number for segment of path1
- o segment2: (number) order number for segment of path2
- o bez1: (array) eight coordinates representing beziér curve for the segment of path1
- o bez2: (array) eight coordinates representing beziér curve for the segment of path2
- o }
- o ]
- \*/
- R.pathIntersection = function (path1, path2) {
- return interPathHelper(path1, path2);
- };
- R.pathIntersectionNumber = function (path1, path2) {
- return interPathHelper(path1, path2, 1);
- };
- function interPathHelper(path1, path2, justCount) {
- path1 = R._path2curve(path1);
- path2 = R._path2curve(path2);
- var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2,
- res = justCount ? 0 : [];
- for (var i = 0, ii = path1.length; i < ii; i++) {
- var pi = path1[i];
- if (pi[0] == "M") {
- x1 = x1m = pi[1];
- y1 = y1m = pi[2];
- } else {
- if (pi[0] == "C") {
- bez1 = [x1, y1].concat(pi.slice(1));
- x1 = bez1[6];
- y1 = bez1[7];
- } else {
- bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m];
- x1 = x1m;
- y1 = y1m;
- }
- for (var j = 0, jj = path2.length; j < jj; j++) {
- var pj = path2[j];
- if (pj[0] == "M") {
- x2 = x2m = pj[1];
- y2 = y2m = pj[2];
- } else {
- if (pj[0] == "C") {
- bez2 = [x2, y2].concat(pj.slice(1));
- x2 = bez2[6];
- y2 = bez2[7];
- } else {
- bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m];
- x2 = x2m;
- y2 = y2m;
- }
- var intr = interHelper(bez1, bez2, justCount);
- if (justCount) {
- res += intr;
- } else {
- for (var k = 0, kk = intr.length; k < kk; k++) {
- intr[k].segment1 = i;
- intr[k].segment2 = j;
- intr[k].bez1 = bez1;
- intr[k].bez2 = bez2;
- }
- res = res.concat(intr);
- }
- }
- }
- }
- }
- return res;
- }
- /*\
- * Raphael.isPointInsidePath
- [ method ]
- **
- * Utility method
- **
- * Returns `true` if given point is inside a given closed path.
- > Parameters
- - path (string) path string
- - x (number) x of the point
- - y (number) y of the point
- = (boolean) true, if point is inside the path
- \*/
- R.isPointInsidePath = function (path, x, y) {
- var bbox = R.pathBBox(path);
- return R.isPointInsideBBox(bbox, x, y) &&
- interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1;
- };
- R._removedFactory = function (methodname) {
- return function () {
- eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname);
- };
- };
- /*\
- * Raphael.pathBBox
- [ method ]
- **
- * Utility method
- **
- * Return bounding box of a given path
- > Parameters
- - path (string) path string
- = (object) bounding box
- o {
- o x: (number) x coordinate of the left top point of the box
- o y: (number) y coordinate of the left top point of the box
- o x2: (number) x coordinate of the right bottom point of the box
- o y2: (number) y coordinate of the right bottom point of the box
- o width: (number) width of the box
- o height: (number) height of the box
- o cx: (number) x coordinate of the center of the box
- o cy: (number) y coordinate of the center of the box
- o }
- \*/
- var pathDimensions = R.pathBBox = function (path) {
- var pth = paths(path);
- if (pth.bbox) {
- return clone(pth.bbox);
- }
- if (!path) {
- return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0};
- }
- path = path2curve(path);
- var x = 0,
- y = 0,
- X = [],
- Y = [],
- p;
- for (var i = 0, ii = path.length; i < ii; i++) {
- p = path[i];
- if (p[0] == "M") {
- x = p[1];
- y = p[2];
- X.push(x);
- Y.push(y);
- } else {
- var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
- X = X[concat](dim.min.x, dim.max.x);
- Y = Y[concat](dim.min.y, dim.max.y);
- x = p[5];
- y = p[6];
- }
- }
- var xmin = mmin[apply](0, X),
- ymin = mmin[apply](0, Y),
- xmax = mmax[apply](0, X),
- ymax = mmax[apply](0, Y),
- width = xmax - xmin,
- height = ymax - ymin,
- bb = {
- x: xmin,
- y: ymin,
- x2: xmax,
- y2: ymax,
- width: width,
- height: height,
- cx: xmin + width / 2,
- cy: ymin + height / 2
- };
- pth.bbox = clone(bb);
- return bb;
- },
- pathClone = function (pathArray) {
- var res = clone(pathArray);
- res.toString = R._path2string;
- return res;
- },
- pathToRelative = R._pathToRelative = function (pathArray) {
- var pth = paths(pathArray);
- if (pth.rel) {
- return pathClone(pth.rel);
- }
- if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
- pathArray = R.parsePathString(pathArray);
- }
- var res = [],
- x = 0,
- y = 0,
- mx = 0,
- my = 0,
- start = 0;
- if (pathArray[0][0] == "M") {
- x = pathArray[0][1];
- y = pathArray[0][2];
- mx = x;
- my = y;
- start++;
- res.push(["M", x, y]);
- }
- for (var i = start, ii = pathArray.length; i < ii; i++) {
- var r = res[i] = [],
- pa = pathArray[i];
- if (pa[0] != lowerCase.call(pa[0])) {
- r[0] = lowerCase.call(pa[0]);
- switch (r[0]) {
- case "a":
- r[1] = pa[1];
- r[2] = pa[2];
- r[3] = pa[3];
- r[4] = pa[4];
- r[5] = pa[5];
- r[6] = +(pa[6] - x).toFixed(3);
- r[7] = +(pa[7] - y).toFixed(3);
- break;
- case "v":
- r[1] = +(pa[1] - y).toFixed(3);
- break;
- case "m":
- mx = pa[1];
- my = pa[2];
- default:
- for (var j = 1, jj = pa.length; j < jj; j++) {
- r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
- }
- }
- } else {
- r = res[i] = [];
- if (pa[0] == "m") {
- mx = pa[1] + x;
- my = pa[2] + y;
- }
- for (var k = 0, kk = pa.length; k < kk; k++) {
- res[i][k] = pa[k];
- }
- }
- var len = res[i].length;
- switch (res[i][0]) {
- case "z":
- x = mx;
- y = my;
- break;
- case "h":
- x += +res[i][len - 1];
- break;
- case "v":
- y += +res[i][len - 1];
- break;
- default:
- x += +res[i][len - 2];
- y += +res[i][len - 1];
- }
- }
- res.toString = R._path2string;
- pth.rel = pathClone(res);
- return res;
- },
- pathToAbsolute = R._pathToAbsolute = function (pathArray) {
- var pth = paths(pathArray);
- if (pth.abs) {
- return pathClone(pth.abs);
- }
- if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
- pathArray = R.parsePathString(pathArray);
- }
- if (!pathArray || !pathArray.length) {
- return [["M", 0, 0]];
- }
- var res = [],
- x = 0,
- y = 0,
- mx = 0,
- my = 0,
- start = 0;
- if (pathArray[0][0] == "M") {
- x = +pathArray[0][1];
- y = +pathArray[0][2];
- mx = x;
- my = y;
- start++;
- res[0] = ["M", x, y];
- }
- var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z";
- for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) {
- res.push(r = []);
- pa = pathArray[i];
- if (pa[0] != upperCase.call(pa[0])) {
- r[0] = upperCase.call(pa[0]);
- switch (r[0]) {
- case "A":
- r[1] = pa[1];
- r[2] = pa[2];
- r[3] = pa[3];
- r[4] = pa[4];
- r[5] = pa[5];
- r[6] = +(pa[6] + x);
- r[7] = +(pa[7] + y);
- break;
- case "V":
- r[1] = +pa[1] + y;
- break;
- case "H":
- r[1] = +pa[1] + x;
- break;
- case "R":
- var dots = [x, y][concat](pa.slice(1));
- for (var j = 2, jj = dots.length; j < jj; j++) {
- dots[j] = +dots[j] + x;
- dots[++j] = +dots[j] + y;
- }
- res.pop();
- res = res[concat](catmullRom2bezier(dots, crz));
- break;
- case "M":
- mx = +pa[1] + x;
- my = +pa[2] + y;
- default:
- for (j = 1, jj = pa.length; j < jj; j++) {
- r[j] = +pa[j] + ((j % 2) ? x : y);
- }
- }
- } else if (pa[0] == "R") {
- dots = [x, y][concat](pa.slice(1));
- res.pop();
- res = res[concat](catmullRom2bezier(dots, crz));
- r = ["R"][concat](pa.slice(-2));
- } else {
- for (var k = 0, kk = pa.length; k < kk; k++) {
- r[k] = pa[k];
- }
- }
- switch (r[0]) {
- case "Z":
- x = mx;
- y = my;
- break;
- case "H":
- x = r[1];
- break;
- case "V":
- y = r[1];
- break;
- case "M":
- mx = r[r.length - 2];
- my = r[r.length - 1];
- default:
- x = r[r.length - 2];
- y = r[r.length - 1];
- }
- }
- res.toString = R._path2string;
- pth.abs = pathClone(res);
- return res;
- },
- l2c = function (x1, y1, x2, y2) {
- return [x1, y1, x2, y2, x2, y2];
- },
- q2c = function (x1, y1, ax, ay, x2, y2) {
- var _13 = 1 / 3,
- _23 = 2 / 3;
- return [
- _13 * x1 + _23 * ax,
- _13 * y1 + _23 * ay,
- _13 * x2 + _23 * ax,
- _13 * y2 + _23 * ay,
- x2,
- y2
- ];
- },
- a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
- // for more information of where this math came from visit:
- // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
- var _120 = PI * 120 / 180,
- rad = PI / 180 * (+angle || 0),
- res = [],
- xy,
- rotate = cacher(function (x, y, rad) {
- var X = x * math.cos(rad) - y * math.sin(rad),
- Y = x * math.sin(rad) + y * math.cos(rad);
- return {x: X, y: Y};
- });
- if (!recursive) {
- xy = rotate(x1, y1, -rad);
- x1 = xy.x;
- y1 = xy.y;
- xy = rotate(x2, y2, -rad);
- x2 = xy.x;
- y2 = xy.y;
- var cos = math.cos(PI / 180 * angle),
- sin = math.sin(PI / 180 * angle),
- x = (x1 - x2) / 2,
- y = (y1 - y2) / 2;
- var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
- if (h > 1) {
- h = math.sqrt(h);
- rx = h * rx;
- ry = h * ry;
- }
- var rx2 = rx * rx,
- ry2 = ry * ry,
- k = (large_arc_flag == sweep_flag ? -1 : 1) *
- math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
- cx = k * rx * y / ry + (x1 + x2) / 2,
- cy = k * -ry * x / rx + (y1 + y2) / 2,
- f1 = math.asin(((y1 - cy) / ry).toFixed(9)),
- f2 = math.asin(((y2 - cy) / ry).toFixed(9));
-
- f1 = x1 < cx ? PI - f1 : f1;
- f2 = x2 < cx ? PI - f2 : f2;
- f1 < 0 && (f1 = PI * 2 + f1);
- f2 < 0 && (f2 = PI * 2 + f2);
- if (sweep_flag && f1 > f2) {
- f1 = f1 - PI * 2;
- }
- if (!sweep_flag && f2 > f1) {
- f2 = f2 - PI * 2;
- }
- } else {
- f1 = recursive[0];
- f2 = recursive[1];
- cx = recursive[2];
- cy = recursive[3];
- }
- var df = f2 - f1;
- if (abs(df) > _120) {
- var f2old = f2,
- x2old = x2,
- y2old = y2;
- f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
- x2 = cx + rx * math.cos(f2);
- y2 = cy + ry * math.sin(f2);
- res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
- }
- df = f2 - f1;
- var c1 = math.cos(f1),
- s1 = math.sin(f1),
- c2 = math.cos(f2),
- s2 = math.sin(f2),
- t = math.tan(df / 4),
- hx = 4 / 3 * rx * t,
- hy = 4 / 3 * ry * t,
- m1 = [x1, y1],
- m2 = [x1 + hx * s1, y1 - hy * c1],
- m3 = [x2 + hx * s2, y2 - hy * c2],
- m4 = [x2, y2];
- m2[0] = 2 * m1[0] - m2[0];
- m2[1] = 2 * m1[1] - m2[1];
- if (recursive) {
- return [m2, m3, m4][concat](res);
- } else {
- res = [m2, m3, m4][concat](res).join()[split](",");
- var newres = [];
- for (var i = 0, ii = res.length; i < ii; i++) {
- newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
- }
- return newres;
- }
- },
- findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
- var t1 = 1 - t;
- return {
- x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
- y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y
- };
- },
- curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
- var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
- b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
- c = p1x - c1x,
- t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a,
- t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a,
- y = [p1y, p2y],
- x = [p1x, p2x],
- dot;
- abs(t1) > "1e12" && (t1 = .5);
- abs(t2) > "1e12" && (t2 = .5);
- if (t1 > 0 && t1 < 1) {
- dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
- x.push(dot.x);
- y.push(dot.y);
- }
- if (t2 > 0 && t2 < 1) {
- dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
- x.push(dot.x);
- y.push(dot.y);
- }
- a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
- b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
- c = p1y - c1y;
- t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a;
- t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a;
- abs(t1) > "1e12" && (t1 = .5);
- abs(t2) > "1e12" && (t2 = .5);
- if (t1 > 0 && t1 < 1) {
- dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
- x.push(dot.x);
- y.push(dot.y);
- }
- if (t2 > 0 && t2 < 1) {
- dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
- x.push(dot.x);
- y.push(dot.y);
- }
- return {
- min: {x: mmin[apply](0, x), y: mmin[apply](0, y)},
- max: {x: mmax[apply](0, x), y: mmax[apply](0, y)}
- };
- }),
- path2curve = R._path2curve = cacher(function (path, path2) {
- var pth = !path2 && paths(path);
- if (!path2 && pth.curve) {
- return pathClone(pth.curve);
- }
- var p = pathToAbsolute(path),
- p2 = path2 && pathToAbsolute(path2),
- attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
- attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
- processPath = function (path, d, pcom) {
- var nx, ny, tq = {T:1, Q:1};
- if (!path) {
- return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
- }
- !(path[0] in tq) && (d.qx = d.qy = null);
- switch (path[0]) {
- case "M":
- d.X = path[1];
- d.Y = path[2];
- break;
- case "A":
- path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1))));
- break;
- case "S":
- if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S.
- nx = d.x * 2 - d.bx; // And reflect the previous
- ny = d.y * 2 - d.by; // command's control point relative to the current point.
- }
- else { // or some else or nothing
- nx = d.x;
- ny = d.y;
- }
- path = ["C", nx, ny][concat](path.slice(1));
- break;
- case "T":
- if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T.
- d.qx = d.x * 2 - d.qx; // And make a reflection similar
- d.qy = d.y * 2 - d.qy; // to case "S".
- }
- else { // or something else or nothing
- d.qx = d.x;
- d.qy = d.y;
- }
- path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
- break;
- case "Q":
- d.qx = path[1];
- d.qy = path[2];
- path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
- break;
- case "L":
- path = ["C"][concat](l2c(d.x, d.y, path[1], path[2]));
- break;
- case "H":
- path = ["C"][concat](l2c(d.x, d.y, path[1], d.y));
- break;
- case "V":
- path = ["C"][concat](l2c(d.x, d.y, d.x, path[1]));
- break;
- case "Z":
- path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y));
- break;
- }
- return path;
- },
- fixArc = function (pp, i) {
- if (pp[i].length > 7) {
- pp[i].shift();
- var pi = pp[i];
- while (pi.length) {
- pcoms1[i]="A"; // if created multiple C:s, their original seg is saved
- p2 && (pcoms2[i]="A"); // the same as above
- pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6)));
- }
- pp.splice(i, 1);
- ii = mmax(p.length, p2 && p2.length || 0);
- }
- },
- fixM = function (path1, path2, a1, a2, i) {
- if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
- path2.splice(i, 0, ["M", a2.x, a2.y]);
- a1.bx = 0;
- a1.by = 0;
- a1.x = path1[i][1];
- a1.y = path1[i][2];
- ii = mmax(p.length, p2 && p2.length || 0);
- }
- },
- pcoms1 = [], // path commands of original path p
- pcoms2 = [], // path commands of original path p2
- pfirst = "", // temporary holder for original path command
- pcom = ""; // holder for previous path command of original path
- for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) {
- p[i] && (pfirst = p[i][0]); // save current path command
-
- if (pfirst != "C") // C is not saved yet, because it may be result of conversion
- {
- pcoms1[i] = pfirst; // Save current path command
- i && ( pcom = pcoms1[i-1]); // Get previous path command pcom
- }
- p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath
-
- if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command
- // which may produce multiple C:s
- // so we have to make sure that C is also C in original path
-
- fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1
-
- if (p2) { // the same procedures is done to p2
- p2[i] && (pfirst = p2[i][0]);
- if (pfirst != "C")
- {
- pcoms2[i] = pfirst;
- i && (pcom = pcoms2[i-1]);
- }
- p2[i] = processPath(p2[i], attrs2, pcom);
-
- if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C";
-
- fixArc(p2, i);
- }
- fixM(p, p2, attrs, attrs2, i);
- fixM(p2, p, attrs2, attrs, i);
- var seg = p[i],
- seg2 = p2 && p2[i],
- seglen = seg.length,
- seg2len = p2 && seg2.length;
- attrs.x = seg[seglen - 2];
- attrs.y = seg[seglen - 1];
- attrs.bx = toFloat(seg[seglen - 4]) || attrs.x;
- attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
- attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x);
- attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y);
- attrs2.x = p2 && seg2[seg2len - 2];
- attrs2.y = p2 && seg2[seg2len - 1];
- }
- if (!p2) {
- pth.curve = pathClone(p);
- }
- return p2 ? [p, p2] : p;
- }, null, pathClone),
- parseDots = R._parseDots = cacher(function (gradient) {
- var dots = [];
- for (var i = 0, ii = gradient.length; i < ii; i++) {
- var dot = {},
- par = gradient[i].match(/^([^:]*):?([\d\.]*)/);
- dot.color = R.getRGB(par[1]);
- if (dot.color.error) {
- return null;
- }
- dot.color = dot.color.hex;
- par[2] && (dot.offset = par[2] + "%");
- dots.push(dot);
- }
- for (i = 1, ii = dots.length - 1; i < ii; i++) {
- if (!dots[i].offset) {
- var start = toFloat(dots[i - 1].offset || 0),
- end = 0;
- for (var j = i + 1; j < ii; j++) {
- if (dots[j].offset) {
- end = dots[j].offset;
- break;
- }
- }
- if (!end) {
- end = 100;
- j = ii;
- }
- end = toFloat(end);
- var d = (end - start) / (j - i + 1);
- for (; i < j; i++) {
- start += d;
- dots[i].offset = start + "%";
- }
- }
- }
- return dots;
- }),
- tear = R._tear = function (el, paper) {
- el == paper.top && (paper.top = el.prev);
- el == paper.bottom && (paper.bottom = el.next);
- el.next && (el.next.prev = el.prev);
- el.prev && (el.prev.next = el.next);
- },
- tofront = R._tofront = function (el, paper) {
- if (paper.top === el) {
- return;
- }
- tear(el, paper);
- el.next = null;
- el.prev = paper.top;
- paper.top.next = el;
- paper.top = el;
- },
- toback = R._toback = function (el, paper) {
- if (paper.bottom === el) {
- return;
- }
- tear(el, paper);
- el.next = paper.bottom;
- el.prev = null;
- paper.bottom.prev = el;
- paper.bottom = el;
- },
- insertafter = R._insertafter = function (el, el2, paper) {
- tear(el, paper);
- el2 == paper.top && (paper.top = el);
- el2.next && (el2.next.prev = el);
- el.next = el2.next;
- el.prev = el2;
- el2.next = el;
- },
- insertbefore = R._insertbefore = function (el, el2, paper) {
- tear(el, paper);
- el2 == paper.bottom && (paper.bottom = el);
- el2.prev && (el2.prev.next = el);
- el.prev = el2.prev;
- el2.prev = el;
- el.next = el2;
- },
- /*\
- * Raphael.toMatrix
- [ method ]
- **
- * Utility method
- **
- * Returns matrix of transformations applied to a given path
- > Parameters
- - path (string) path string
- - transform (string|array) transformation string
- = (object) @Matrix
- \*/
- toMatrix = R.toMatrix = function (path, transform) {
- var bb = pathDimensions(path),
- el = {
- _: {
- transform: E
- },
- getBBox: function () {
- return bb;
- }
- };
- extractTransform(el, transform);
- return el.matrix;
- },
- /*\
- * Raphael.transformPath
- [ method ]
- **
- * Utility method
- **
- * Returns path transformed by a given transformation
- > Parameters
- - path (string) path string
- - transform (string|array) transformation string
- = (string) path
- \*/
- transformPath = R.transformPath = function (path, transform) {
- return mapPath(path, toMatrix(path, transform));
- },
- extractTransform = R._extractTransform = function (el, tstr) {
- if (tstr == null) {
- return el._.transform;
- }
- tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E);
- var tdata = R.parseTransformString(tstr),
- deg = 0,
- dx = 0,
- dy = 0,
- sx = 1,
- sy = 1,
- _ = el._,
- m = new Matrix;
- _.transform = tdata || [];
- if (tdata) {
- for (var i = 0, ii = tdata.length; i < ii; i++) {
- var t = tdata[i],
- tlen = t.length,
- command = Str(t[0]).toLowerCase(),
- absolute = t[0] != command,
- inver = absolute ? m.invert() : 0,
- x1,
- y1,
- x2,
- y2,
- bb;
- if (command == "t" && tlen == 3) {
- if (absolute) {
- x1 = inver.x(0, 0);
- y1 = inver.y(0, 0);
- x2 = inver.x(t[1], t[2]);
- y2 = inver.y(t[1], t[2]);
- m.translate(x2 - x1, y2 - y1);
- } else {
- m.translate(t[1], t[2]);
- }
- } else if (command == "r") {
- if (tlen == 2) {
- bb = bb || el.getBBox(1);
- m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2);
- deg += t[1];
- } else if (tlen == 4) {
- if (absolute) {
- x2 = inver.x(t[2], t[3]);
- y2 = inver.y(t[2], t[3]);
- m.rotate(t[1], x2, y2);
- } else {
- m.rotate(t[1], t[2], t[3]);
- }
- deg += t[1];
- }
- } else if (command == "s") {
- if (tlen == 2 || tlen == 3) {
- bb = bb || el.getBBox(1);
- m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2);
- sx *= t[1];
- sy *= t[tlen - 1];
- } else if (tlen == 5) {
- if (absolute) {
- x2 = inver.x(t[3], t[4]);
- y2 = inver.y(t[3], t[4]);
- m.scale(t[1], t[2], x2, y2);
- } else {
- m.scale(t[1], t[2], t[3], t[4]);
- }
- sx *= t[1];
- sy *= t[2];
- }
- } else if (command == "m" && tlen == 7) {
- m.add(t[1], t[2], t[3], t[4], t[5], t[6]);
- }
- _.dirtyT = 1;
- el.matrix = m;
- }
- }
-
- /*\
- * Element.matrix
- [ property (object) ]
- **
- * Keeps @Matrix object, which represents element transformation
- \*/
- el.matrix = m;
-
- _.sx = sx;
- _.sy = sy;
- _.deg = deg;
- _.dx = dx = m.e;
- _.dy = dy = m.f;
-
- if (sx == 1 && sy == 1 && !deg && _.bbox) {
- _.bbox.x += +dx;
- _.bbox.y += +dy;
- } else {
- _.dirtyT = 1;
- }
- },
- getEmpty = function (item) {
- var l = item[0];
- switch (l.toLowerCase()) {
- case "t": return [l, 0, 0];
- case "m": return [l, 1, 0, 0, 1, 0, 0];
- case "r": if (item.length == 4) {
- return [l, 0, item[2], item[3]];
- } else {
- return [l, 0];
- }
- case "s": if (item.length == 5) {
- return [l, 1, 1, item[3], item[4]];
- } else if (item.length == 3) {
- return [l, 1, 1];
- } else {
- return [l, 1];
- }
- }
- },
- equaliseTransform = R._equaliseTransform = function (t1, t2) {
- t2 = Str(t2).replace(/\.{3}|\u2026/g, t1);
- t1 = R.parseTransformString(t1) || [];
- t2 = R.parseTransformString(t2) || [];
- var maxlength = mmax(t1.length, t2.length),
- from = [],
- to = [],
- i = 0, j, jj,
- tt1, tt2;
- for (; i < maxlength; i++) {
- tt1 = t1[i] || getEmpty(t2[i]);
- tt2 = t2[i] || getEmpty(tt1);
- if ((tt1[0] != tt2[0]) ||
- (tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) ||
- (tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4]))
- ) {
- return;
- }
- from[i] = [];
- to[i] = [];
- for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) {
- j in tt1 && (from[i][j] = tt1[j]);
- j in tt2 && (to[i][j] = tt2[j]);
- }
- }
- return {
- from: from,
- to: to
- };
- };
- R._getContainer = function (x, y, w, h) {
- var container;
- container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x;
- if (container == null) {
- return;
- }
- if (container.tagName) {
- if (y == null) {
- return {
- container: container,
- width: container.style.pixelWidth || container.offsetWidth,
- height: container.style.pixelHeight || container.offsetHeight
- };
- } else {
- return {
- container: container,
- width: y,
- height: w
- };
- }
- }
- return {
- container: 1,
- x: x,
- y: y,
- width: w,
- height: h
- };
- };
- /*\
- * Raphael.pathToRelative
- [ method ]
- **
- * Utility method
- **
- * Converts path to relative form
- > Parameters
- - pathString (string|array) path string or array of segments
- = (array) array of segments.
- \*/
- R.pathToRelative = pathToRelative;
- R._engine = {};
- /*\
- * Raphael.path2curve
- [ method ]
- **
- * Utility method
- **
- * Converts path to a new path where all segments are cubic bezier curves.
- > Parameters
- - pathString (string|array) path string or array of segments
- = (array) array of segments.
- \*/
- R.path2curve = path2curve;
- /*\
- * Raphael.matrix
- [ method ]
- **
- * Utility method
- **
- * Returns matrix based on given parameters.
- > Parameters
- - a (number)
- - b (number)
- - c (number)
- - d (number)
- - e (number)
- - f (number)
- = (object) @Matrix
- \*/
- R.matrix = function (a, b, c, d, e, f) {
- return new Matrix(a, b, c, d, e, f);
- };
- function Matrix(a, b, c, d, e, f) {
- if (a != null) {
- this.a = +a;
- this.b = +b;
- this.c = +c;
- this.d = +d;
- this.e = +e;
- this.f = +f;
- } else {
- this.a = 1;
- this.b = 0;
- this.c = 0;
- this.d = 1;
- this.e = 0;
- this.f = 0;
- }
- }
- (function (matrixproto) {
- /*\
- * Matrix.add
- [ method ]
- **
- * Adds given matrix to existing one.
- > Parameters
- - a (number)
- - b (number)
- - c (number)
- - d (number)
- - e (number)
- - f (number)
- or
- - matrix (object) @Matrix
- \*/
- matrixproto.add = function (a, b, c, d, e, f) {
- var out = [[], [], []],
- m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]],
- matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
- x, y, z, res;
-
- if (a && a instanceof Matrix) {
- matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]];
- }
-
- for (x = 0; x < 3; x++) {
- for (y = 0; y < 3; y++) {
- res = 0;
- for (z = 0; z < 3; z++) {
- res += m[x][z] * matrix[z][y];
- }
- out[x][y] = res;
- }
- }
- this.a = out[0][0];
- this.b = out[1][0];
- this.c = out[0][1];
- this.d = out[1][1];
- this.e = out[0][2];
- this.f = out[1][2];
- };
- /*\
- * Matrix.invert
- [ method ]
- **
- * Returns inverted version of the matrix
- = (object) @Matrix
- \*/
- matrixproto.invert = function () {
- var me = this,
- x = me.a * me.d - me.b * me.c;
- return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x);
- };
- /*\
- * Matrix.clone
- [ method ]
- **
- * Returns copy of the matrix
- = (object) @Matrix
- \*/
- matrixproto.clone = function () {
- return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f);
- };
- /*\
- * Matrix.translate
- [ method ]
- **
- * Translate the matrix
- > Parameters
- - x (number)
- - y (number)
- \*/
- matrixproto.translate = function (x, y) {
- this.add(1, 0, 0, 1, x, y);
- };
- /*\
- * Matrix.scale
- [ method ]
- **
- * Scales the matrix
- > Parameters
- - x (number)
- - y (number) #optional
- - cx (number) #optional
- - cy (number) #optional
- \*/
- matrixproto.scale = function (x, y, cx, cy) {
- y == null && (y = x);
- (cx || cy) && this.add(1, 0, 0, 1, cx, cy);
- this.add(x, 0, 0, y, 0, 0);
- (cx || cy) && this.add(1, 0, 0, 1, -cx, -cy);
- };
- /*\
- * Matrix.rotate
- [ method ]
- **
- * Rotates the matrix
- > Parameters
- - a (number)
- - x (number)
- - y (number)
- \*/
- matrixproto.rotate = function (a, x, y) {
- a = R.rad(a);
- x = x || 0;
- y = y || 0;
- var cos = +math.cos(a).toFixed(9),
- sin = +math.sin(a).toFixed(9);
- this.add(cos, sin, -sin, cos, x, y);
- this.add(1, 0, 0, 1, -x, -y);
- };
- /*\
- * Matrix.x
- [ method ]
- **
- * Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y
- > Parameters
- - x (number)
- - y (number)
- = (number) x
- \*/
- matrixproto.x = function (x, y) {
- return x * this.a + y * this.c + this.e;
- };
- /*\
- * Matrix.y
- [ method ]
- **
- * Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x
- > Parameters
- - x (number)
- - y (number)
- = (number) y
- \*/
- matrixproto.y = function (x, y) {
- return x * this.b + y * this.d + this.f;
- };
- matrixproto.get = function (i) {
- return +this[Str.fromCharCode(97 + i)].toFixed(4);
- };
- matrixproto.toString = function () {
- return R.svg ?
- "matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" :
- [this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join();
- };
- matrixproto.toFilter = function () {
- return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) +
- ", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) +
- ", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')";
- };
- matrixproto.offset = function () {
- return [this.e.toFixed(4), this.f.toFixed(4)];
- };
- function norm(a) {
- return a[0] * a[0] + a[1] * a[1];
- }
- function normalize(a) {
- var mag = math.sqrt(norm(a));
- a[0] && (a[0] /= mag);
- a[1] && (a[1] /= mag);
- }
- /*\
- * Matrix.split
- [ method ]
- **
- * Splits matrix into primitive transformations
- = (object) in format:
- o dx (number) translation by x
- o dy (number) translation by y
- o scalex (number) scale by x
- o scaley (number) scale by y
- o shear (number) shear
- o rotate (number) rotation in deg
- o isSimple (boolean) could it be represented via simple transformations
- \*/
- matrixproto.split = function () {
- var out = {};
- // translation
- out.dx = this.e;
- out.dy = this.f;
-
- // scale and shear
- var row = [[this.a, this.c], [this.b, this.d]];
- out.scalex = math.sqrt(norm(row[0]));
- normalize(row[0]);
-
- out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
- row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
-
- out.scaley = math.sqrt(norm(row[1]));
- normalize(row[1]);
- out.shear /= out.scaley;
-
- // rotation
- var sin = -row[0][1],
- cos = row[1][1];
- if (cos < 0) {
- out.rotate = R.deg(math.acos(cos));
- if (sin < 0) {
- out.rotate = 360 - out.rotate;
- }
- } else {
- out.rotate = R.deg(math.asin(sin));
- }
-
- out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate);
- out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate;
- out.noRotation = !+out.shear.toFixed(9) && !out.rotate;
- return out;
- };
- /*\
- * Matrix.toTransformString
- [ method ]
- **
- * Return transform string that represents given matrix
- = (string) transform string
- \*/
- matrixproto.toTransformString = function (shorter) {
- var s = shorter || this[split]();
- if (s.isSimple) {
- s.scalex = +s.scalex.toFixed(4);
- s.scaley = +s.scaley.toFixed(4);
- s.rotate = +s.rotate.toFixed(4);
- return (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) +
- (s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) +
- (s.rotate ? "r" + [s.rotate, 0, 0] : E);
- } else {
- return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)];
- }
- };
- })(Matrix.prototype);
-
- // WebKit rendering bug workaround method
- var version = navigator.userAgent.match(/Version\/(.*?)\s/) || navigator.userAgent.match(/Chrome\/(\d+)/);
- if ((navigator.vendor == "Apple Computer, Inc.") && (version && version[1] < 4 || navigator.platform.slice(0, 2) == "iP") ||
- (navigator.vendor == "Google Inc." && version && version[1] < 8)) {
- /*\
- * Paper.safari
- [ method ]
- **
- * There is an inconvenient rendering bug in Safari (WebKit):
- * sometimes the rendering should be forced.
- * This method should help with dealing with this bug.
- \*/
- paperproto.safari = function () {
- var rect = this.rect(-99, -99, this.width + 99, this.height + 99).attr({stroke: "none"});
- setTimeout(function () {rect.remove();});
- };
- } else {
- paperproto.safari = fun;
- }
-
- var preventDefault = function () {
- this.returnValue = false;
- },
- preventTouch = function () {
- return this.originalEvent.preventDefault();
- },
- stopPropagation = function () {
- this.cancelBubble = true;
- },
- stopTouch = function () {
- return this.originalEvent.stopPropagation();
- },
- getEventPosition = function (e) {
- var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
- scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
-
- return {
- x: e.clientX + scrollX,
- y: e.clientY + scrollY
- };
- },
- addEvent = (function () {
- if (g.doc.addEventListener) {
- return function (obj, type, fn, element) {
- var f = function (e) {
- var pos = getEventPosition(e);
- return fn.call(element, e, pos.x, pos.y);
- };
- obj.addEventListener(type, f, false);
-
- if (supportsTouch && touchMap[type]) {
- var _f = function (e) {
- var pos = getEventPosition(e),
- olde = e;
-
- for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) {
- if (e.targetTouches[i].target == obj) {
- e = e.targetTouches[i];
- e.originalEvent = olde;
- e.preventDefault = preventTouch;
- e.stopPropagation = stopTouch;
- break;
- }
- }
-
- return fn.call(element, e, pos.x, pos.y);
- };
- obj.addEventListener(touchMap[type], _f, false);
- }
-
- return function () {
- obj.removeEventListener(type, f, false);
-
- if (supportsTouch && touchMap[type])
- obj.removeEventListener(touchMap[type], _f, false);
-
- return true;
- };
- };
- } else if (g.doc.attachEvent) {
- return function (obj, type, fn, element) {
- var f = function (e) {
- e = e || g.win.event;
- var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
- scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
- x = e.clientX + scrollX,
- y = e.clientY + scrollY;
- e.preventDefault = e.preventDefault || preventDefault;
- e.stopPropagation = e.stopPropagation || stopPropagation;
- return fn.call(element, e, x, y);
- };
- obj.attachEvent("on" + type, f);
- var detacher = function () {
- obj.detachEvent("on" + type, f);
- return true;
- };
- return detacher;
- };
- }
- })(),
- drag = [],
- dragMove = function (e) {
- var x = e.clientX,
- y = e.clientY,
- scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
- scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
- dragi,
- j = drag.length;
- while (j--) {
- dragi = drag[j];
- if (supportsTouch && e.touches) {
- var i = e.touches.length,
- touch;
- while (i--) {
- touch = e.touches[i];
- if (touch.identifier == dragi.el._drag.id) {
- x = touch.clientX;
- y = touch.clientY;
- (e.originalEvent ? e.originalEvent : e).preventDefault();
- break;
- }
- }
- } else {
- e.preventDefault();
- }
- var node = dragi.el.node,
- o,
- next = node.nextSibling,
- parent = node.parentNode,
- display = node.style.display;
- g.win.opera && parent.removeChild(node);
- node.style.display = "none";
- o = dragi.el.paper.getElementByPoint(x, y);
- node.style.display = display;
- g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node));
- o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o);
- x += scrollX;
- y += scrollY;
- eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e);
- }
- },
- dragUp = function (e) {
- R.unmousemove(dragMove).unmouseup(dragUp);
- var i = drag.length,
- dragi;
- while (i--) {
- dragi = drag[i];
- dragi.el._drag = {};
- eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e);
- }
- drag = [];
- },
- /*\
- * Raphael.el
- [ property (object) ]
- **
- * You can add your own method to elements. This is usefull when you want to hack default functionality or
- * want to wrap some common transformation or attributes in one method. In difference to canvas methods,
- * you can redefine element method at any time. Expending element methods wouldn’t affect set.
- > Usage
- | Raphael.el.red = function () {
- | this.attr({fill: "#f00"});
- | };
- | // then use it
- | paper.circle(100, 100, 20).red();
- \*/
- elproto = R.el = {};
- /*\
- * Element.click
- [ method ]
- **
- * Adds event handler for click for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unclick
- [ method ]
- **
- * Removes event handler for click for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.dblclick
- [ method ]
- **
- * Adds event handler for double click for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.undblclick
- [ method ]
- **
- * Removes event handler for double click for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.mousedown
- [ method ]
- **
- * Adds event handler for mousedown for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unmousedown
- [ method ]
- **
- * Removes event handler for mousedown for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.mousemove
- [ method ]
- **
- * Adds event handler for mousemove for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unmousemove
- [ method ]
- **
- * Removes event handler for mousemove for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.mouseout
- [ method ]
- **
- * Adds event handler for mouseout for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unmouseout
- [ method ]
- **
- * Removes event handler for mouseout for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.mouseover
- [ method ]
- **
- * Adds event handler for mouseover for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unmouseover
- [ method ]
- **
- * Removes event handler for mouseover for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.mouseup
- [ method ]
- **
- * Adds event handler for mouseup for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.unmouseup
- [ method ]
- **
- * Removes event handler for mouseup for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.touchstart
- [ method ]
- **
- * Adds event handler for touchstart for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.untouchstart
- [ method ]
- **
- * Removes event handler for touchstart for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.touchmove
- [ method ]
- **
- * Adds event handler for touchmove for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.untouchmove
- [ method ]
- **
- * Removes event handler for touchmove for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.touchend
- [ method ]
- **
- * Adds event handler for touchend for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.untouchend
- [ method ]
- **
- * Removes event handler for touchend for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
-
- /*\
- * Element.touchcancel
- [ method ]
- **
- * Adds event handler for touchcancel for the element.
- > Parameters
- - handler (function) handler for the event
- = (object) @Element
- \*/
- /*\
- * Element.untouchcancel
- [ method ]
- **
- * Removes event handler for touchcancel for the element.
- > Parameters
- - handler (function) #optional handler for the event
- = (object) @Element
- \*/
- for (var i = events.length; i--;) {
- (function (eventName) {
- R[eventName] = elproto[eventName] = function (fn, scope) {
- if (R.is(fn, "function")) {
- this.events = this.events || [];
- this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)});
- }
- return this;
- };
- R["un" + eventName] = elproto["un" + eventName] = function (fn) {
- var events = this.events || [],
- l = events.length;
- while (l--){
- if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) {
- events[l].unbind();
- events.splice(l, 1);
- !events.length && delete this.events;
- }
- }
- return this;
- };
- })(events[i]);
- }
-
- /*\
- * Element.data
- [ method ]
- **
- * Adds or retrieves given value asociated with given key.
- **
- * See also @Element.removeData
- > Parameters
- - key (string) key to store data
- - value (any) #optional value to store
- = (object) @Element
- * or, if value is not specified:
- = (any) value
- * or, if key and value are not specified:
- = (object) Key/value pairs for all the data associated with the element.
- > Usage
- | for (var i = 0, i < 5, i++) {
- | paper.circle(10 + 15 * i, 10, 10)
- | .attr({fill: "#000"})
- | .data("i", i)
- | .click(function () {
- | alert(this.data("i"));
- | });
- | }
- \*/
- elproto.data = function (key, value) {
- var data = eldata[this.id] = eldata[this.id] || {};
- if (arguments.length == 0) {
- return data;
- }
- if (arguments.length == 1) {
- if (R.is(key, "object")) {
- for (var i in key) if (key[has](i)) {
- this.data(i, key[i]);
- }
- return this;
- }
- eve("raphael.data.get." + this.id, this, data[key], key);
- return data[key];
- }
- data[key] = value;
- eve("raphael.data.set." + this.id, this, value, key);
- return this;
- };
- /*\
- * Element.removeData
- [ method ]
- **
- * Removes value associated with an element by given key.
- * If key is not provided, removes all the data of the element.
- > Parameters
- - key (string) #optional key
- = (object) @Element
- \*/
- elproto.removeData = function (key) {
- if (key == null) {
- eldata[this.id] = {};
- } else {
- eldata[this.id] && delete eldata[this.id][key];
- }
- return this;
- };
- /*\
- * Element.getData
- [ method ]
- **
- * Retrieves the element data
- = (object) data
- \*/
- elproto.getData = function () {
- return clone(eldata[this.id] || {});
- };
- /*\
- * Element.hover
- [ method ]
- **
- * Adds event handlers for hover for the element.
- > Parameters
- - f_in (function) handler for hover in
- - f_out (function) handler for hover out
- - icontext (object) #optional context for hover in handler
- - ocontext (object) #optional context for hover out handler
- = (object) @Element
- \*/
- elproto.hover = function (f_in, f_out, scope_in, scope_out) {
- return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in);
- };
- /*\
- * Element.unhover
- [ method ]
- **
- * Removes event handlers for hover for the element.
- > Parameters
- - f_in (function) handler for hover in
- - f_out (function) handler for hover out
- = (object) @Element
- \*/
- elproto.unhover = function (f_in, f_out) {
- return this.unmouseover(f_in).unmouseout(f_out);
- };
- var draggable = [];
- /*\
- * Element.drag
- [ method ]
- **
- * Adds event handlers for drag of the element.
- > Parameters
- - onmove (function) handler for moving
- - onstart (function) handler for drag start
- - onend (function) handler for drag end
- - mcontext (object) #optional context for moving handler
- - scontext (object) #optional context for drag start handler
- - econtext (object) #optional context for drag end handler
- * Additionaly following `drag` events will be triggered: `drag.start.<id>` on start,
- * `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element
- * `drag.over.<id>` will be fired as well.
- *
- * Start event and start handler will be called in specified context or in context of the element with following parameters:
- o x (number) x position of the mouse
- o y (number) y position of the mouse
- o event (object) DOM event object
- * Move event and move handler will be called in specified context or in context of the element with following parameters:
- o dx (number) shift by x from the start point
- o dy (number) shift by y from the start point
- o x (number) x position of the mouse
- o y (number) y position of the mouse
- o event (object) DOM event object
- * End event and end handler will be called in specified context or in context of the element with following parameters:
- o event (object) DOM event object
- = (object) @Element
- \*/
- elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) {
- function start(e) {
- (e.originalEvent || e).preventDefault();
- var x = e.clientX,
- y = e.clientY,
- scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
- scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
- this._drag.id = e.identifier;
- if (supportsTouch && e.touches) {
- var i = e.touches.length, touch;
- while (i--) {
- touch = e.touches[i];
- this._drag.id = touch.identifier;
- if (touch.identifier == this._drag.id) {
- x = touch.clientX;
- y = touch.clientY;
- break;
- }
- }
- }
- this._drag.x = x + scrollX;
- this._drag.y = y + scrollY;
- !drag.length && R.mousemove(dragMove).mouseup(dragUp);
- drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope});
- onstart && eve.on("raphael.drag.start." + this.id, onstart);
- onmove && eve.on("raphael.drag.move." + this.id, onmove);
- onend && eve.on("raphael.drag.end." + this.id, onend);
- eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e);
- }
- this._drag = {};
- draggable.push({el: this, start: start});
- this.mousedown(start);
- return this;
- };
- /*\
- * Element.onDragOver
- [ method ]
- **
- * Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id).
- > Parameters
- - f (function) handler for event, first argument would be the element you are dragging over
- \*/
- elproto.onDragOver = function (f) {
- f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id);
- };
- /*\
- * Element.undrag
- [ method ]
- **
- * Removes all drag event handlers from given element.
- \*/
- elproto.undrag = function () {
- var i = draggable.length;
- while (i--) if (draggable[i].el == this) {
- this.unmousedown(draggable[i].start);
- draggable.splice(i, 1);
- eve.unbind("raphael.drag.*." + this.id);
- }
- !draggable.length && R.unmousemove(dragMove).unmouseup(dragUp);
- drag = [];
- };
- /*\
- * Paper.circle
- [ method ]
- **
- * Draws a circle.
- **
- > Parameters
- **
- - x (number) x coordinate of the centre
- - y (number) y coordinate of the centre
- - r (number) radius
- = (object) Raphaël element object with type “circle”
- **
- > Usage
- | var c = paper.circle(50, 50, 40);
- \*/
- paperproto.circle = function (x, y, r) {
- var out = R._engine.circle(this, x || 0, y || 0, r || 0);
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.rect
- [ method ]
- *
- * Draws a rectangle.
- **
- > Parameters
- **
- - x (number) x coordinate of the top left corner
- - y (number) y coordinate of the top left corner
- - width (number) width
- - height (number) height
- - r (number) #optional radius for rounded corners, default is 0
- = (object) Raphaël element object with type “rect”
- **
- > Usage
- | // regular rectangle
- | var c = paper.rect(10, 10, 50, 50);
- | // rectangle with rounded corners
- | var c = paper.rect(40, 40, 50, 50, 10);
- \*/
- paperproto.rect = function (x, y, w, h, r) {
- var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0);
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.ellipse
- [ method ]
- **
- * Draws an ellipse.
- **
- > Parameters
- **
- - x (number) x coordinate of the centre
- - y (number) y coordinate of the centre
- - rx (number) horizontal radius
- - ry (number) vertical radius
- = (object) Raphaël element object with type “ellipse”
- **
- > Usage
- | var c = paper.ellipse(50, 50, 40, 20);
- \*/
- paperproto.ellipse = function (x, y, rx, ry) {
- var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0);
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.path
- [ method ]
- **
- * Creates a path element by given path data string.
- > Parameters
- - pathString (string) #optional path string in SVG format.
- * Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example:
- | "M10,20L30,40"
- * Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative.
- *
- # <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p>
- # <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody>
- # <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr>
- # <tr><td>Z</td><td>closepath</td><td>(none)</td></tr>
- # <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr>
- # <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr>
- # <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr>
- # <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr>
- # <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr>
- # <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr>
- # <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr>
- # <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr>
- # <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table>
- * * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier.
- * Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning.
- > Usage
- | var c = paper.path("M10 10L90 90");
- | // draw a diagonal line:
- | // move to 10,10, line to 90,90
- * For example of path strings, check out these icons: http://raphaeljs.com/icons/
- \*/
- paperproto.path = function (pathString) {
- pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E);
- var out = R._engine.path(R.format[apply](R, arguments), this);
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.image
- [ method ]
- **
- * Embeds an image into the surface.
- **
- > Parameters
- **
- - src (string) URI of the source image
- - x (number) x coordinate position
- - y (number) y coordinate position
- - width (number) width of the image
- - height (number) height of the image
- = (object) Raphaël element object with type “image”
- **
- > Usage
- | var c = paper.image("apple.png", 10, 10, 80, 80);
- \*/
- paperproto.image = function (src, x, y, w, h) {
- var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.text
- [ method ]
- **
- * Draws a text string. If you need line breaks, put “\n” in the string.
- **
- > Parameters
- **
- - x (number) x coordinate position
- - y (number) y coordinate position
- - text (string) The text string to draw
- = (object) Raphaël element object with type “text”
- **
- > Usage
- | var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!");
- \*/
- paperproto.text = function (x, y, text) {
- var out = R._engine.text(this, x || 0, y || 0, Str(text));
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Paper.set
- [ method ]
- **
- * Creates array-like object to keep and operate several elements at once.
- * Warning: it doesn’t create any elements for itself in the page, it just groups existing elements.
- * Sets act as pseudo elements — all methods available to an element can be used on a set.
- = (object) array-like object that represents set of elements
- **
- > Usage
- | var st = paper.set();
- | st.push(
- | paper.circle(10, 10, 5),
- | paper.circle(30, 10, 5)
- | );
- | st.attr({fill: "red"}); // changes the fill of both circles
- \*/
- paperproto.set = function (itemsArray) {
- !R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length));
- var out = new Set(itemsArray);
- this.__set__ && this.__set__.push(out);
- out["paper"] = this;
- out["type"] = "set";
- return out;
- };
- /*\
- * Paper.setStart
- [ method ]
- **
- * Creates @Paper.set. All elements that will be created after calling this method and before calling
- * @Paper.setFinish will be added to the set.
- **
- > Usage
- | paper.setStart();
- | paper.circle(10, 10, 5),
- | paper.circle(30, 10, 5)
- | var st = paper.setFinish();
- | st.attr({fill: "red"}); // changes the fill of both circles
- \*/
- paperproto.setStart = function (set) {
- this.__set__ = set || this.set();
- };
- /*\
- * Paper.setFinish
- [ method ]
- **
- * See @Paper.setStart. This method finishes catching and returns resulting set.
- **
- = (object) set
- \*/
- paperproto.setFinish = function (set) {
- var out = this.__set__;
- delete this.__set__;
- return out;
- };
- /*\
- * Paper.getSize
- [ method ]
- **
- * Obtains current paper actual size.
- **
- = (object)
- \*/
- paperproto.getSize = function () {
- var container = this.canvas.parentNode;
- return {
- width: container.offsetWidth,
- height: container.offsetHeight
- };
- };
- /*\
- * Paper.setSize
- [ method ]
- **
- * If you need to change dimensions of the canvas call this method
- **
- > Parameters
- **
- - width (number) new width of the canvas
- - height (number) new height of the canvas
- \*/
- paperproto.setSize = function (width, height) {
- return R._engine.setSize.call(this, width, height);
- };
- /*\
- * Paper.setViewBox
- [ method ]
- **
- * Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by
- * specifying new boundaries.
- **
- > Parameters
- **
- - x (number) new x position, default is `0`
- - y (number) new y position, default is `0`
- - w (number) new width of the canvas
- - h (number) new height of the canvas
- - fit (boolean) `true` if you want graphics to fit into new boundary box
- \*/
- paperproto.setViewBox = function (x, y, w, h, fit) {
- return R._engine.setViewBox.call(this, x, y, w, h, fit);
- };
- /*\
- * Paper.top
- [ property ]
- **
- * Points to the topmost element on the paper
- \*/
- /*\
- * Paper.bottom
- [ property ]
- **
- * Points to the bottom element on the paper
- \*/
- paperproto.top = paperproto.bottom = null;
- /*\
- * Paper.raphael
- [ property ]
- **
- * Points to the @Raphael object/function
- \*/
- paperproto.raphael = R;
- var getOffset = function (elem) {
- var box = elem.getBoundingClientRect(),
- doc = elem.ownerDocument,
- body = doc.body,
- docElem = doc.documentElement,
- clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
- top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop,
- left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft;
- return {
- y: top,
- x: left
- };
- };
- /*\
- * Paper.getElementByPoint
- [ method ]
- **
- * Returns you topmost element under given point.
- **
- = (object) Raphaël element object
- > Parameters
- **
- - x (number) x coordinate from the top left corner of the window
- - y (number) y coordinate from the top left corner of the window
- > Usage
- | paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"});
- \*/
- paperproto.getElementByPoint = function (x, y) {
- var paper = this,
- svg = paper.canvas,
- target = g.doc.elementFromPoint(x, y);
- if (g.win.opera && target.tagName == "svg") {
- var so = getOffset(svg),
- sr = svg.createSVGRect();
- sr.x = x - so.x;
- sr.y = y - so.y;
- sr.width = sr.height = 1;
- var hits = svg.getIntersectionList(sr, null);
- if (hits.length) {
- target = hits[hits.length - 1];
- }
- }
- if (!target) {
- return null;
- }
- while (target.parentNode && target != svg.parentNode && !target.raphael) {
- target = target.parentNode;
- }
- target == paper.canvas.parentNode && (target = svg);
- target = target && target.raphael ? paper.getById(target.raphaelid) : null;
- return target;
- };
-
- /*\
- * Paper.getElementsByBBox
- [ method ]
- **
- * Returns set of elements that have an intersecting bounding box
- **
- > Parameters
- **
- - bbox (object) bbox to check with
- = (object) @Set
- \*/
- paperproto.getElementsByBBox = function (bbox) {
- var set = this.set();
- this.forEach(function (el) {
- if (R.isBBoxIntersect(el.getBBox(), bbox)) {
- set.push(el);
- }
- });
- return set;
- };
-
- /*\
- * Paper.getById
- [ method ]
- **
- * Returns you element by its internal ID.
- **
- > Parameters
- **
- - id (number) id
- = (object) Raphaël element object
- \*/
- paperproto.getById = function (id) {
- var bot = this.bottom;
- while (bot) {
- if (bot.id == id) {
- return bot;
- }
- bot = bot.next;
- }
- return null;
- };
- /*\
- * Paper.forEach
- [ method ]
- **
- * Executes given function for each element on the paper
- *
- * If callback function returns `false` it will stop loop running.
- **
- > Parameters
- **
- - callback (function) function to run
- - thisArg (object) context object for the callback
- = (object) Paper object
- > Usage
- | paper.forEach(function (el) {
- | el.attr({ stroke: "blue" });
- | });
- \*/
- paperproto.forEach = function (callback, thisArg) {
- var bot = this.bottom;
- while (bot) {
- if (callback.call(thisArg, bot) === false) {
- return this;
- }
- bot = bot.next;
- }
- return this;
- };
- /*\
- * Paper.getElementsByPoint
- [ method ]
- **
- * Returns set of elements that have common point inside
- **
- > Parameters
- **
- - x (number) x coordinate of the point
- - y (number) y coordinate of the point
- = (object) @Set
- \*/
- paperproto.getElementsByPoint = function (x, y) {
- var set = this.set();
- this.forEach(function (el) {
- if (el.isPointInside(x, y)) {
- set.push(el);
- }
- });
- return set;
- };
- function x_y() {
- return this.x + S + this.y;
- }
- function x_y_w_h() {
- return this.x + S + this.y + S + this.width + " \xd7 " + this.height;
- }
- /*\
- * Element.isPointInside
- [ method ]
- **
- * Determine if given point is inside this element’s shape
- **
- > Parameters
- **
- - x (number) x coordinate of the point
- - y (number) y coordinate of the point
- = (boolean) `true` if point inside the shape
- \*/
- elproto.isPointInside = function (x, y) {
- var rp = this.realPath = getPath[this.type](this);
- if (this.attr('transform') && this.attr('transform').length) {
- rp = R.transformPath(rp, this.attr('transform'));
- }
- return R.isPointInsidePath(rp, x, y);
- };
- /*\
- * Element.getBBox
- [ method ]
- **
- * Return bounding box for a given element
- **
- > Parameters
- **
- - isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`.
- = (object) Bounding box object:
- o {
- o x: (number) top left corner x
- o y: (number) top left corner y
- o x2: (number) bottom right corner x
- o y2: (number) bottom right corner y
- o width: (number) width
- o height: (number) height
- o }
- \*/
- elproto.getBBox = function (isWithoutTransform) {
- if (this.removed) {
- return {};
- }
- var _ = this._;
- if (isWithoutTransform) {
- if (_.dirty || !_.bboxwt) {
- this.realPath = getPath[this.type](this);
- _.bboxwt = pathDimensions(this.realPath);
- _.bboxwt.toString = x_y_w_h;
- _.dirty = 0;
- }
- return _.bboxwt;
- }
- if (_.dirty || _.dirtyT || !_.bbox) {
- if (_.dirty || !this.realPath) {
- _.bboxwt = 0;
- this.realPath = getPath[this.type](this);
- }
- _.bbox = pathDimensions(mapPath(this.realPath, this.matrix));
- _.bbox.toString = x_y_w_h;
- _.dirty = _.dirtyT = 0;
- }
- return _.bbox;
- };
- /*\
- * Element.clone
- [ method ]
- **
- = (object) clone of a given element
- **
- \*/
- elproto.clone = function () {
- if (this.removed) {
- return null;
- }
- var out = this.paper[this.type]().attr(this.attr());
- this.__set__ && this.__set__.push(out);
- return out;
- };
- /*\
- * Element.glow
- [ method ]
- **
- * Return set of elements that create glow-like effect around given element. See @Paper.set.
- *
- * Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself.
- **
- > Parameters
- **
- - glow (object) #optional parameters object with all properties optional:
- o {
- o width (number) size of the glow, default is `10`
- o fill (boolean) will it be filled, default is `false`
- o opacity (number) opacity, default is `0.5`
- o offsetx (number) horizontal offset, default is `0`
- o offsety (number) vertical offset, default is `0`
- o color (string) glow colour, default is `black`
- o }
- = (object) @Paper.set of elements that represents glow
- \*/
- elproto.glow = function (glow) {
- if (this.type == "text") {
- return null;
- }
- glow = glow || {};
- var s = {
- width: (glow.width || 10) + (+this.attr("stroke-width") || 1),
- fill: glow.fill || false,
- opacity: glow.opacity || .5,
- offsetx: glow.offsetx || 0,
- offsety: glow.offsety || 0,
- color: glow.color || "#000"
- },
- c = s.width / 2,
- r = this.paper,
- out = r.set(),
- path = this.realPath || getPath[this.type](this);
- path = this.matrix ? mapPath(path, this.matrix) : path;
- for (var i = 1; i < c + 1; i++) {
- out.push(r.path(path).attr({
- stroke: s.color,
- fill: s.fill ? s.color : "none",
- "stroke-linejoin": "round",
- "stroke-linecap": "round",
- "stroke-width": +(s.width / c * i).toFixed(3),
- opacity: +(s.opacity / c).toFixed(3)
- }));
- }
- return out.insertBefore(this).translate(s.offsetx, s.offsety);
- };
- var curveslengths = {},
- getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
- if (length == null) {
- return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
- } else {
- return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
- }
- },
- getLengthFactory = function (istotal, subpath) {
- return function (path, length, onlystart) {
- path = path2curve(path);
- var x, y, p, l, sp = "", subpaths = {}, point,
- len = 0;
- for (var i = 0, ii = path.length; i < ii; i++) {
- p = path[i];
- if (p[0] == "M") {
- x = +p[1];
- y = +p[2];
- } else {
- l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
- if (len + l > length) {
- if (subpath && !subpaths.start) {
- point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
- sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
- if (onlystart) {return sp;}
- subpaths.start = sp;
- sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join();
- len += l;
- x = +p[5];
- y = +p[6];
- continue;
- }
- if (!istotal && !subpath) {
- point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
- return {x: point.x, y: point.y, alpha: point.alpha};
- }
- }
- len += l;
- x = +p[5];
- y = +p[6];
- }
- sp += p.shift() + p;
- }
- subpaths.end = sp;
- point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
- point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
- return point;
- };
- };
- var getTotalLength = getLengthFactory(1),
- getPointAtLength = getLengthFactory(),
- getSubpathsAtLength = getLengthFactory(0, 1);
- /*\
- * Raphael.getTotalLength
- [ method ]
- **
- * Returns length of the given path in pixels.
- **
- > Parameters
- **
- - path (string) SVG path string.
- **
- = (number) length.
- \*/
- R.getTotalLength = getTotalLength;
- /*\
- * Raphael.getPointAtLength
- [ method ]
- **
- * Return coordinates of the point located at the given length on the given path.
- **
- > Parameters
- **
- - path (string) SVG path string
- - length (number)
- **
- = (object) representation of the point:
- o {
- o x: (number) x coordinate
- o y: (number) y coordinate
- o alpha: (number) angle of derivative
- o }
- \*/
- R.getPointAtLength = getPointAtLength;
- /*\
- * Raphael.getSubpath
- [ method ]
- **
- * Return subpath of a given path from given length to given length.
- **
- > Parameters
- **
- - path (string) SVG path string
- - from (number) position of the start of the segment
- - to (number) position of the end of the segment
- **
- = (string) pathstring for the segment
- \*/
- R.getSubpath = function (path, from, to) {
- if (this.getTotalLength(path) - to < 1e-6) {
- return getSubpathsAtLength(path, from).end;
- }
- var a = getSubpathsAtLength(path, to, 1);
- return from ? getSubpathsAtLength(a, from).end : a;
- };
- /*\
- * Element.getTotalLength
- [ method ]
- **
- * Returns length of the path in pixels. Only works for element of “path” type.
- = (number) length.
- \*/
- elproto.getTotalLength = function () {
- var path = this.getPath();
- if (!path) {
- return;
- }
-
- if (this.node.getTotalLength) {
- return this.node.getTotalLength();
- }
-
- return getTotalLength(path);
- };
- /*\
- * Element.getPointAtLength
- [ method ]
- **
- * Return coordinates of the point located at the given length on the given path. Only works for element of “path” type.
- **
- > Parameters
- **
- - length (number)
- **
- = (object) representation of the point:
- o {
- o x: (number) x coordinate
- o y: (number) y coordinate
- o alpha: (number) angle of derivative
- o }
- \*/
- elproto.getPointAtLength = function (length) {
- var path = this.getPath();
- if (!path) {
- return;
- }
-
- return getPointAtLength(path, length);
- };
- /*\
- * Element.getPath
- [ method ]
- **
- * Returns path of the element. Only works for elements of “path” type and simple elements like circle.
- = (object) path
- **
- \*/
- elproto.getPath = function () {
- var path,
- getPath = R._getPath[this.type];
-
- if (this.type == "text" || this.type == "set") {
- return;
- }
-
- if (getPath) {
- path = getPath(this);
- }
-
- return path;
- };
- /*\
- * Element.getSubpath
- [ method ]
- **
- * Return subpath of a given element from given length to given length. Only works for element of “path” type.
- **
- > Parameters
- **
- - from (number) position of the start of the segment
- - to (number) position of the end of the segment
- **
- = (string) pathstring for the segment
- \*/
- elproto.getSubpath = function (from, to) {
- var path = this.getPath();
- if (!path) {
- return;
- }
-
- return R.getSubpath(path, from, to);
- };
- /*\
- * Raphael.easing_formulas
- [ property ]
- **
- * Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing:
- # <ul>
- # <li>“linear”</li>
- # <li>“&lt;” or “easeIn” or “ease-in”</li>
- # <li>“>” or “easeOut” or “ease-out”</li>
- # <li>“&lt;>” or “easeInOut” or “ease-in-out”</li>
- # <li>“backIn” or “back-in”</li>
- # <li>“backOut” or “back-out”</li>
- # <li>“elastic”</li>
- # <li>“bounce”</li>
- # </ul>
- # <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p>
- \*/
- var ef = R.easing_formulas = {
- linear: function (n) {
- return n;
- },
- "<": function (n) {
- return pow(n, 1.7);
- },
- ">": function (n) {
- return pow(n, .48);
- },
- "<>": function (n) {
- var q = .48 - n / 1.04,
- Q = math.sqrt(.1734 + q * q),
- x = Q - q,
- X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
- y = -Q - q,
- Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
- t = X + Y + .5;
- return (1 - t) * 3 * t * t + t * t * t;
- },
- backIn: function (n) {
- var s = 1.70158;
- return n * n * ((s + 1) * n - s);
- },
- backOut: function (n) {
- n = n - 1;
- var s = 1.70158;
- return n * n * ((s + 1) * n + s) + 1;
- },
- elastic: function (n) {
- if (n == !!n) {
- return n;
- }
- return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1;
- },
- bounce: function (n) {
- var s = 7.5625,
- p = 2.75,
- l;
- if (n < (1 / p)) {
- l = s * n * n;
- } else {
- if (n < (2 / p)) {
- n -= (1.5 / p);
- l = s * n * n + .75;
- } else {
- if (n < (2.5 / p)) {
- n -= (2.25 / p);
- l = s * n * n + .9375;
- } else {
- n -= (2.625 / p);
- l = s * n * n + .984375;
- }
- }
- }
- return l;
- }
- };
- ef.easeIn = ef["ease-in"] = ef["<"];
- ef.easeOut = ef["ease-out"] = ef[">"];
- ef.easeInOut = ef["ease-in-out"] = ef["<>"];
- ef["back-in"] = ef.backIn;
- ef["back-out"] = ef.backOut;
-
- var animationElements = [],
- requestAnimFrame = window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame ||
- function (callback) {
- setTimeout(callback, 16);
- },
- animation = function () {
- var Now = +new Date,
- l = 0;
- for (; l < animationElements.length; l++) {
- var e = animationElements[l];
- if (e.el.removed || e.paused) {
- continue;
- }
- var time = Now - e.start,
- ms = e.ms,
- easing = e.easing,
- from = e.from,
- diff = e.diff,
- to = e.to,
- t = e.t,
- that = e.el,
- set = {},
- now,
- init = {},
- key;
- if (e.initstatus) {
- time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms;
- e.status = e.initstatus;
- delete e.initstatus;
- e.stop && animationElements.splice(l--, 1);
- } else {
- e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top;
- }
- if (time < 0) {
- continue;
- }
- if (time < ms) {
- var pos = easing(time / ms);
- for (var attr in from) if (from[has](attr)) {
- switch (availableAnimAttrs[attr]) {
- case nu:
- now = +from[attr] + pos * ms * diff[attr];
- break;
- case "colour":
- now = "rgb(" + [
- upto255(round(from[attr].r + pos * ms * diff[attr].r)),
- upto255(round(from[attr].g + pos * ms * diff[attr].g)),
- upto255(round(from[attr].b + pos * ms * diff[attr].b))
- ].join(",") + ")";
- break;
- case "path":
- now = [];
- for (var i = 0, ii = from[attr].length; i < ii; i++) {
- now[i] = [from[attr][i][0]];
- for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
- now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
- }
- now[i] = now[i].join(S);
- }
- now = now.join(S);
- break;
- case "transform":
- if (diff[attr].real) {
- now = [];
- for (i = 0, ii = from[attr].length; i < ii; i++) {
- now[i] = [from[attr][i][0]];
- for (j = 1, jj = from[attr][i].length; j < jj; j++) {
- now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j];
- }
- }
- } else {
- var get = function (i) {
- return +from[attr][i] + pos * ms * diff[attr][i];
- };
- // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]];
- now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]];
- }
- break;
- case "csv":
- if (attr == "clip-rect") {
- now = [];
- i = 4;
- while (i--) {
- now[i] = +from[attr][i] + pos * ms * diff[attr][i];
- }
- }
- break;
- default:
- var from2 = [][concat](from[attr]);
- now = [];
- i = that.paper.customAttributes[attr].length;
- while (i--) {
- now[i] = +from2[i] + pos * ms * diff[attr][i];
- }
- break;
- }
- set[attr] = now;
- }
- that.attr(set);
- (function (id, that, anim) {
- setTimeout(function () {
- eve("raphael.anim.frame." + id, that, anim);
- });
- })(that.id, that, e.anim);
- } else {
- (function(f, el, a) {
- setTimeout(function() {
- eve("raphael.anim.frame." + el.id, el, a);
- eve("raphael.anim.finish." + el.id, el, a);
- R.is(f, "function") && f.call(el);
- });
- })(e.callback, that, e.anim);
- that.attr(to);
- animationElements.splice(l--, 1);
- if (e.repeat > 1 && !e.next) {
- for (key in to) if (to[has](key)) {
- init[key] = e.totalOrigin[key];
- }
- e.el.attr(init);
- runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1);
- }
- if (e.next && !e.stop) {
- runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat);
- }
- }
- }
- R.svg && that && that.paper && that.paper.safari();
- animationElements.length && requestAnimFrame(animation);
- },
- upto255 = function (color) {
- return color > 255 ? 255 : color < 0 ? 0 : color;
- };
- /*\
- * Element.animateWith
- [ method ]
- **
- * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element.
- **
- > Parameters
- **
- - el (object) element to sync with
- - anim (object) animation to sync with
- - params (object) #optional final attributes for the element, see also @Element.attr
- - ms (number) #optional number of milliseconds for animation to run
- - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
- - callback (function) #optional callback function. Will be called at the end of animation.
- * or
- - element (object) element to sync with
- - anim (object) animation to sync with
- - animation (object) #optional animation object, see @Raphael.animation
- **
- = (object) original element
- \*/
- elproto.animateWith = function (el, anim, params, ms, easing, callback) {
- var element = this;
- if (element.removed) {
- callback && callback.call(element);
- return element;
- }
- var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback),
- x, y;
- runAnimation(a, element, a.percents[0], null, element.attr());
- for (var i = 0, ii = animationElements.length; i < ii; i++) {
- if (animationElements[i].anim == anim && animationElements[i].el == el) {
- animationElements[ii - 1].start = animationElements[i].start;
- break;
- }
- }
- return element;
- //
- //
- // var a = params ? R.animation(params, ms, easing, callback) : anim,
- // status = element.status(anim);
- // return this.animate(a).status(a, status * anim.ms / a.ms);
- };
- function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) {
- var cx = 3 * p1x,
- bx = 3 * (p2x - p1x) - cx,
- ax = 1 - cx - bx,
- cy = 3 * p1y,
- by = 3 * (p2y - p1y) - cy,
- ay = 1 - cy - by;
- function sampleCurveX(t) {
- return ((ax * t + bx) * t + cx) * t;
- }
- function solve(x, epsilon) {
- var t = solveCurveX(x, epsilon);
- return ((ay * t + by) * t + cy) * t;
- }
- function solveCurveX(x, epsilon) {
- var t0, t1, t2, x2, d2, i;
- for(t2 = x, i = 0; i < 8; i++) {
- x2 = sampleCurveX(t2) - x;
- if (abs(x2) < epsilon) {
- return t2;
- }
- d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
- if (abs(d2) < 1e-6) {
- break;
- }
- t2 = t2 - x2 / d2;
- }
- t0 = 0;
- t1 = 1;
- t2 = x;
- if (t2 < t0) {
- return t0;
- }
- if (t2 > t1) {
- return t1;
- }
- while (t0 < t1) {
- x2 = sampleCurveX(t2);
- if (abs(x2 - x) < epsilon) {
- return t2;
- }
- if (x > x2) {
- t0 = t2;
- } else {
- t1 = t2;
- }
- t2 = (t1 - t0) / 2 + t0;
- }
- return t2;
- }
- return solve(t, 1 / (200 * duration));
- }
- elproto.onAnimation = function (f) {
- f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id);
- return this;
- };
- function Animation(anim, ms) {
- var percents = [],
- newAnim = {};
- this.ms = ms;
- this.times = 1;
- if (anim) {
- for (var attr in anim) if (anim[has](attr)) {
- newAnim[toFloat(attr)] = anim[attr];
- percents.push(toFloat(attr));
- }
- percents.sort(sortByNumber);
- }
- this.anim = newAnim;
- this.top = percents[percents.length - 1];
- this.percents = percents;
- }
- /*\
- * Animation.delay
- [ method ]
- **
- * Creates a copy of existing animation object with given delay.
- **
- > Parameters
- **
- - delay (number) number of ms to pass between animation start and actual animation
- **
- = (object) new altered Animation object
- | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3);
- | circle1.animate(anim); // run the given animation immediately
- | circle2.animate(anim.delay(500)); // run the given animation after 500 ms
- \*/
- Animation.prototype.delay = function (delay) {
- var a = new Animation(this.anim, this.ms);
- a.times = this.times;
- a.del = +delay || 0;
- return a;
- };
- /*\
- * Animation.repeat
- [ method ]
- **
- * Creates a copy of existing animation object with given repetition.
- **
- > Parameters
- **
- - repeat (number) number iterations of animation. For infinite animation pass `Infinity`
- **
- = (object) new altered Animation object
- \*/
- Animation.prototype.repeat = function (times) {
- var a = new Animation(this.anim, this.ms);
- a.del = this.del;
- a.times = math.floor(mmax(times, 0)) || 1;
- return a;
- };
- function runAnimation(anim, element, percent, status, totalOrigin, times) {
- percent = toFloat(percent);
- var params,
- isInAnim,
- isInAnimSet,
- percents = [],
- next,
- prev,
- timestamp,
- ms = anim.ms,
- from = {},
- to = {},
- diff = {};
- if (status) {
- for (i = 0, ii = animationElements.length; i < ii; i++) {
- var e = animationElements[i];
- if (e.el.id == element.id && e.anim == anim) {
- if (e.percent != percent) {
- animationElements.splice(i, 1);
- isInAnimSet = 1;
- } else {
- isInAnim = e;
- }
- element.attr(e.totalOrigin);
- break;
- }
- }
- } else {
- status = +to; // NaN
- }
- for (var i = 0, ii = anim.percents.length; i < ii; i++) {
- if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) {
- percent = anim.percents[i];
- prev = anim.percents[i - 1] || 0;
- ms = ms / anim.top * (percent - prev);
- next = anim.percents[i + 1];
- params = anim.anim[percent];
- break;
- } else if (status) {
- element.attr(anim.anim[anim.percents[i]]);
- }
- }
- if (!params) {
- return;
- }
- if (!isInAnim) {
- for (var attr in params) if (params[has](attr)) {
- if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) {
- from[attr] = element.attr(attr);
- (from[attr] == null) && (from[attr] = availableAttrs[attr]);
- to[attr] = params[attr];
- switch (availableAnimAttrs[attr]) {
- case nu:
- diff[attr] = (to[attr] - from[attr]) / ms;
- break;
- case "colour":
- from[attr] = R.getRGB(from[attr]);
- var toColour = R.getRGB(to[attr]);
- diff[attr] = {
- r: (toColour.r - from[attr].r) / ms,
- g: (toColour.g - from[attr].g) / ms,
- b: (toColour.b - from[attr].b) / ms
- };
- break;
- case "path":
- var pathes = path2curve(from[attr], to[attr]),
- toPath = pathes[1];
- from[attr] = pathes[0];
- diff[attr] = [];
- for (i = 0, ii = from[attr].length; i < ii; i++) {
- diff[attr][i] = [0];
- for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
- diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
- }
- }
- break;
- case "transform":
- var _ = element._,
- eq = equaliseTransform(_[attr], to[attr]);
- if (eq) {
- from[attr] = eq.from;
- to[attr] = eq.to;
- diff[attr] = [];
- diff[attr].real = true;
- for (i = 0, ii = from[attr].length; i < ii; i++) {
- diff[attr][i] = [from[attr][i][0]];
- for (j = 1, jj = from[attr][i].length; j < jj; j++) {
- diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms;
- }
- }
- } else {
- var m = (element.matrix || new Matrix),
- to2 = {
- _: {transform: _.transform},
- getBBox: function () {
- return element.getBBox(1);
- }
- };
- from[attr] = [
- m.a,
- m.b,
- m.c,
- m.d,
- m.e,
- m.f
- ];
- extractTransform(to2, to[attr]);
- to[attr] = to2._.transform;
- diff[attr] = [
- (to2.matrix.a - m.a) / ms,
- (to2.matrix.b - m.b) / ms,
- (to2.matrix.c - m.c) / ms,
- (to2.matrix.d - m.d) / ms,
- (to2.matrix.e - m.e) / ms,
- (to2.matrix.f - m.f) / ms
- ];
- // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy];
- // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }};
- // extractTransform(to2, to[attr]);
- // diff[attr] = [
- // (to2._.sx - _.sx) / ms,
- // (to2._.sy - _.sy) / ms,
- // (to2._.deg - _.deg) / ms,
- // (to2._.dx - _.dx) / ms,
- // (to2._.dy - _.dy) / ms
- // ];
- }
- break;
- case "csv":
- var values = Str(params[attr])[split](separator),
- from2 = Str(from[attr])[split](separator);
- if (attr == "clip-rect") {
- from[attr] = from2;
- diff[attr] = [];
- i = from2.length;
- while (i--) {
- diff[attr][i] = (values[i] - from[attr][i]) / ms;
- }
- }
- to[attr] = values;
- break;
- default:
- values = [][concat](params[attr]);
- from2 = [][concat](from[attr]);
- diff[attr] = [];
- i = element.paper.customAttributes[attr].length;
- while (i--) {
- diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms;
- }
- break;
- }
- }
- }
- var easing = params.easing,
- easyeasy = R.easing_formulas[easing];
- if (!easyeasy) {
- easyeasy = Str(easing).match(bezierrg);
- if (easyeasy && easyeasy.length == 5) {
- var curve = easyeasy;
- easyeasy = function (t) {
- return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms);
- };
- } else {
- easyeasy = pipe;
- }
- }
- timestamp = params.start || anim.start || +new Date;
- e = {
- anim: anim,
- percent: percent,
- timestamp: timestamp,
- start: timestamp + (anim.del || 0),
- status: 0,
- initstatus: status || 0,
- stop: false,
- ms: ms,
- easing: easyeasy,
- from: from,
- diff: diff,
- to: to,
- el: element,
- callback: params.callback,
- prev: prev,
- next: next,
- repeat: times || anim.times,
- origin: element.attr(),
- totalOrigin: totalOrigin
- };
- animationElements.push(e);
- if (status && !isInAnim && !isInAnimSet) {
- e.stop = true;
- e.start = new Date - ms * status;
- if (animationElements.length == 1) {
- return animation();
- }
- }
- if (isInAnimSet) {
- e.start = new Date - e.ms * status;
- }
- animationElements.length == 1 && requestAnimFrame(animation);
- } else {
- isInAnim.initstatus = status;
- isInAnim.start = new Date - isInAnim.ms * status;
- }
- eve("raphael.anim.start." + element.id, element, anim);
- }
- /*\
- * Raphael.animation
- [ method ]
- **
- * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods.
- * See also @Animation.delay and @Animation.repeat methods.
- **
- > Parameters
- **
- - params (object) final attributes for the element, see also @Element.attr
- - ms (number) number of milliseconds for animation to run
- - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
- - callback (function) #optional callback function. Will be called at the end of animation.
- **
- = (object) @Animation
- \*/
- R.animation = function (params, ms, easing, callback) {
- if (params instanceof Animation) {
- return params;
- }
- if (R.is(easing, "function") || !easing) {
- callback = callback || easing || null;
- easing = null;
- }
- params = Object(params);
- ms = +ms || 0;
- var p = {},
- json,
- attr;
- for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) {
- json = true;
- p[attr] = params[attr];
- }
- if (!json) {
- // if percent-like syntax is used and end-of-all animation callback used
- if(callback){
- // find the last one
- var lastKey = 0;
- for(var i in params){
- var percent = toInt(i);
- if(params[has](i) && percent > lastKey){
- lastKey = percent;
- }
- }
- lastKey += '%';
- // if already defined callback in the last keyframe, skip
- !params[lastKey].callback && (params[lastKey].callback = callback);
- }
- return new Animation(params, ms);
- } else {
- easing && (p.easing = easing);
- callback && (p.callback = callback);
- return new Animation({100: p}, ms);
- }
- };
- /*\
- * Element.animate
- [ method ]
- **
- * Creates and starts animation for given element.
- **
- > Parameters
- **
- - params (object) final attributes for the element, see also @Element.attr
- - ms (number) number of milliseconds for animation to run
- - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
- - callback (function) #optional callback function. Will be called at the end of animation.
- * or
- - animation (object) animation object, see @Raphael.animation
- **
- = (object) original element
- \*/
- elproto.animate = function (params, ms, easing, callback) {
- var element = this;
- if (element.removed) {
- callback && callback.call(element);
- return element;
- }
- var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback);
- runAnimation(anim, element, anim.percents[0], null, element.attr());
- return element;
- };
- /*\
- * Element.setTime
- [ method ]
- **
- * Sets the status of animation of the element in milliseconds. Similar to @Element.status method.
- **
- > Parameters
- **
- - anim (object) animation object
- - value (number) number of milliseconds from the beginning of the animation
- **
- = (object) original element if `value` is specified
- * Note, that during animation following events are triggered:
- *
- * On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`.
- \*/
- elproto.setTime = function (anim, value) {
- if (anim && value != null) {
- this.status(anim, mmin(value, anim.ms) / anim.ms);
- }
- return this;
- };
- /*\
- * Element.status
- [ method ]
- **
- * Gets or sets the status of animation of the element.
- **
- > Parameters
- **
- - anim (object) #optional animation object
- - value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position.
- **
- = (number) status
- * or
- = (array) status if `anim` is not specified. Array of objects in format:
- o {
- o anim: (object) animation object
- o status: (number) status
- o }
- * or
- = (object) original element if `value` is specified
- \*/
- elproto.status = function (anim, value) {
- var out = [],
- i = 0,
- len,
- e;
- if (value != null) {
- runAnimation(anim, this, -1, mmin(value, 1));
- return this;
- } else {
- len = animationElements.length;
- for (; i < len; i++) {
- e = animationElements[i];
- if (e.el.id == this.id && (!anim || e.anim == anim)) {
- if (anim) {
- return e.status;
- }
- out.push({
- anim: e.anim,
- status: e.status
- });
- }
- }
- if (anim) {
- return 0;
- }
- return out;
- }
- };
- /*\
- * Element.pause
- [ method ]
- **
- * Stops animation of the element with ability to resume it later on.
- **
- > Parameters
- **
- - anim (object) #optional animation object
- **
- = (object) original element
- \*/
- elproto.pause = function (anim) {
- for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
- if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) {
- animationElements[i].paused = true;
- }
- }
- return this;
- };
- /*\
- * Element.resume
- [ method ]
- **
- * Resumes animation if it was paused with @Element.pause method.
- **
- > Parameters
- **
- - anim (object) #optional animation object
- **
- = (object) original element
- \*/
- elproto.resume = function (anim) {
- for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
- var e = animationElements[i];
- if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) {
- delete e.paused;
- this.status(e.anim, e.status);
- }
- }
- return this;
- };
- /*\
- * Element.stop
- [ method ]
- **
- * Stops animation of the element.
- **
- > Parameters
- **
- - anim (object) #optional animation object
- **
- = (object) original element
- \*/
- elproto.stop = function (anim) {
- for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
- if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) {
- animationElements.splice(i--, 1);
- }
- }
- return this;
- };
- function stopAnimation(paper) {
- for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) {
- animationElements.splice(i--, 1);
- }
- }
- eve.on("raphael.remove", stopAnimation);
- eve.on("raphael.clear", stopAnimation);
- elproto.toString = function () {
- return "Rapha\xebl\u2019s object";
- };
-
- // Set
- var Set = function (items) {
- this.items = [];
- this.length = 0;
- this.type = "set";
- if (items) {
- for (var i = 0, ii = items.length; i < ii; i++) {
- if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) {
- this[this.items.length] = this.items[this.items.length] = items[i];
- this.length++;
- }
- }
- }
- },
- setproto = Set.prototype;
- /*\
- * Set.push
- [ method ]
- **
- * Adds each argument to the current set.
- = (object) original element
- \*/
- setproto.push = function () {
- var item,
- len;
- for (var i = 0, ii = arguments.length; i < ii; i++) {
- item = arguments[i];
- if (item && (item.constructor == elproto.constructor || item.constructor == Set)) {
- len = this.items.length;
- this[len] = this.items[len] = item;
- this.length++;
- }
- }
- return this;
- };
- /*\
- * Set.pop
- [ method ]
- **
- * Removes last element and returns it.
- = (object) element
- \*/
- setproto.pop = function () {
- this.length && delete this[this.length--];
- return this.items.pop();
- };
- /*\
- * Set.forEach
- [ method ]
- **
- * Executes given function for each element in the set.
- *
- * If function returns `false` it will stop loop running.
- **
- > Parameters
- **
- - callback (function) function to run
- - thisArg (object) context object for the callback
- = (object) Set object
- \*/
- setproto.forEach = function (callback, thisArg) {
- for (var i = 0, ii = this.items.length; i < ii; i++) {
- if (callback.call(thisArg, this.items[i], i) === false) {
- return this;
- }
- }
- return this;
- };
- for (var method in elproto) if (elproto[has](method)) {
- setproto[method] = (function (methodname) {
- return function () {
- var arg = arguments;
- return this.forEach(function (el) {
- el[methodname][apply](el, arg);
- });
- };
- })(method);
- }
- setproto.attr = function (name, value) {
- if (name && R.is(name, array) && R.is(name[0], "object")) {
- for (var j = 0, jj = name.length; j < jj; j++) {
- this.items[j].attr(name[j]);
- }
- } else {
- for (var i = 0, ii = this.items.length; i < ii; i++) {
- this.items[i].attr(name, value);
- }
- }
- return this;
- };
- /*\
- * Set.clear
- [ method ]
- **
- * Removes all elements from the set
- \*/
- setproto.clear = function () {
- while (this.length) {
- this.pop();
- }
- };
- /*\
- * Set.splice
- [ method ]
- **
- * Removes given element from the set
- **
- > Parameters
- **
- - index (number) position of the deletion
- - count (number) number of element to remove
- - insertion… (object) #optional elements to insert
- = (object) set elements that were deleted
- \*/
- setproto.splice = function (index, count, insertion) {
- index = index < 0 ? mmax(this.length + index, 0) : index;
- count = mmax(0, mmin(this.length - index, count));
- var tail = [],
- todel = [],
- args = [],
- i;
- for (i = 2; i < arguments.length; i++) {
- args.push(arguments[i]);
- }
- for (i = 0; i < count; i++) {
- todel.push(this[index + i]);
- }
- for (; i < this.length - index; i++) {
- tail.push(this[index + i]);
- }
- var arglen = args.length;
- for (i = 0; i < arglen + tail.length; i++) {
- this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen];
- }
- i = this.items.length = this.length -= count - arglen;
- while (this[i]) {
- delete this[i++];
- }
- return new Set(todel);
- };
- /*\
- * Set.exclude
- [ method ]
- **
- * Removes given element from the set
- **
- > Parameters
- **
- - element (object) element to remove
- = (boolean) `true` if object was found & removed from the set
- \*/
- setproto.exclude = function (el) {
- for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) {
- this.splice(i, 1);
- return true;
- }
- };
- setproto.animate = function (params, ms, easing, callback) {
- (R.is(easing, "function") || !easing) && (callback = easing || null);
- var len = this.items.length,
- i = len,
- item,
- set = this,
- collector;
- if (!len) {
- return this;
- }
- callback && (collector = function () {
- !--len && callback.call(set);
- });
- easing = R.is(easing, string) ? easing : collector;
- var anim = R.animation(params, ms, easing, collector);
- item = this.items[--i].animate(anim);
- while (i--) {
- this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim);
- (this.items[i] && !this.items[i].removed) || len--;
- }
- return this;
- };
- setproto.insertAfter = function (el) {
- var i = this.items.length;
- while (i--) {
- this.items[i].insertAfter(el);
- }
- return this;
- };
- setproto.getBBox = function () {
- var x = [],
- y = [],
- x2 = [],
- y2 = [];
- for (var i = this.items.length; i--;) if (!this.items[i].removed) {
- var box = this.items[i].getBBox();
- x.push(box.x);
- y.push(box.y);
- x2.push(box.x + box.width);
- y2.push(box.y + box.height);
- }
- x = mmin[apply](0, x);
- y = mmin[apply](0, y);
- x2 = mmax[apply](0, x2);
- y2 = mmax[apply](0, y2);
- return {
- x: x,
- y: y,
- x2: x2,
- y2: y2,
- width: x2 - x,
- height: y2 - y
- };
- };
- setproto.clone = function (s) {
- s = this.paper.set();
- for (var i = 0, ii = this.items.length; i < ii; i++) {
- s.push(this.items[i].clone());
- }
- return s;
- };
- setproto.toString = function () {
- return "Rapha\xebl\u2018s set";
- };
-
- setproto.glow = function(glowConfig) {
- var ret = this.paper.set();
- this.forEach(function(shape, index){
- var g = shape.glow(glowConfig);
- if(g != null){
- g.forEach(function(shape2, index2){
- ret.push(shape2);
- });
- }
- });
- return ret;
- };
-
-
- /*\
- * Set.isPointInside
- [ method ]
- **
- * Determine if given point is inside this set’s elements
- **
- > Parameters
- **
- - x (number) x coordinate of the point
- - y (number) y coordinate of the point
- = (boolean) `true` if point is inside any of the set's elements
- \*/
- setproto.isPointInside = function (x, y) {
- var isPointInside = false;
- this.forEach(function (el) {
- if (el.isPointInside(x, y)) {
- isPointInside = true;
- return false; // stop loop
- }
- });
- return isPointInside;
- };
-
- /*\
- * Raphael.registerFont
- [ method ]
- **
- * Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file.
- * Returns original parameter, so it could be used with chaining.
- # <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a>
- **
- > Parameters
- **
- - font (object) the font to register
- = (object) the font you passed in
- > Usage
- | Cufon.registerFont(Raphael.registerFont({…}));
- \*/
- R.registerFont = function (font) {
- if (!font.face) {
- return font;
- }
- this.fonts = this.fonts || {};
- var fontcopy = {
- w: font.w,
- face: {},
- glyphs: {}
- },
- family = font.face["font-family"];
- for (var prop in font.face) if (font.face[has](prop)) {
- fontcopy.face[prop] = font.face[prop];
- }
- if (this.fonts[family]) {
- this.fonts[family].push(fontcopy);
- } else {
- this.fonts[family] = [fontcopy];
- }
- if (!font.svg) {
- fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
- for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
- var path = font.glyphs[glyph];
- fontcopy.glyphs[glyph] = {
- w: path.w,
- k: {},
- d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) {
- return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
- }) + "z"
- };
- if (path.k) {
- for (var k in path.k) if (path[has](k)) {
- fontcopy.glyphs[glyph].k[k] = path.k[k];
- }
- }
- }
- }
- return font;
- };
- /*\
- * Paper.getFont
- [ method ]
- **
- * Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”.
- **
- > Parameters
- **
- - family (string) font family name or any word from it
- - weight (string) #optional font weight
- - style (string) #optional font style
- - stretch (string) #optional font stretch
- = (object) the font object
- > Usage
- | paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
- \*/
- paperproto.getFont = function (family, weight, style, stretch) {
- stretch = stretch || "normal";
- style = style || "normal";
- weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
- if (!R.fonts) {
- return;
- }
- var font = R.fonts[family];
- if (!font) {
- var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
- for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
- if (name.test(fontName)) {
- font = R.fonts[fontName];
- break;
- }
- }
- }
- var thefont;
- if (font) {
- for (var i = 0, ii = font.length; i < ii; i++) {
- thefont = font[i];
- if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
- break;
- }
- }
- }
- return thefont;
- };
- /*\
- * Paper.print
- [ method ]
- **
- * Creates path that represent given text written using given font at given position with given size.
- * Result of the method is path element that contains whole text as a separate path.
- **
- > Parameters
- **
- - x (number) x position of the text
- - y (number) y position of the text
- - string (string) text to print
- - font (object) font object, see @Paper.getFont
- - size (number) #optional size of the font, default is `16`
- - origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"`
- - letter_spacing (number) #optional number in range `-1..1`, default is `0`
- - line_spacing (number) #optional number in range `1..3`, default is `1`
- = (object) resulting path element, which consist of all letters
- > Usage
- | var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"});
- \*/
- paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) {
- origin = origin || "middle"; // baseline|middle
- letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1);
- line_spacing = mmax(mmin(line_spacing || 1, 3), 1);
- var letters = Str(string)[split](E),
- shift = 0,
- notfirst = 0,
- path = E,
- scale;
- R.is(font, "string") && (font = this.getFont(font));
- if (font) {
- scale = (size || 16) / font.face["units-per-em"];
- var bb = font.face.bbox[split](separator),
- top = +bb[0],
- lineHeight = bb[3] - bb[1],
- shifty = 0,
- height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2);
- for (var i = 0, ii = letters.length; i < ii; i++) {
- if (letters[i] == "\n") {
- shift = 0;
- curr = 0;
- notfirst = 0;
- shifty += lineHeight * line_spacing;
- } else {
- var prev = notfirst && font.glyphs[letters[i - 1]] || {},
- curr = font.glyphs[letters[i]];
- shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0;
- notfirst = 1;
- }
- if (curr && curr.d) {
- path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]);
- }
- }
- }
- return this.path(path).attr({
- fill: "#000",
- stroke: "none"
- });
- };
-
- /*\
- * Paper.add
- [ method ]
- **
- * Imports elements in JSON array in format `{type: type, <attributes>}`
- **
- > Parameters
- **
- - json (array)
- = (object) resulting set of imported elements
- > Usage
- | paper.add([
- | {
- | type: "circle",
- | cx: 10,
- | cy: 10,
- | r: 5
- | },
- | {
- | type: "rect",
- | x: 10,
- | y: 10,
- | width: 10,
- | height: 10,
- | fill: "#fc0"
- | }
- | ]);
- \*/
- paperproto.add = function (json) {
- if (R.is(json, "array")) {
- var res = this.set(),
- i = 0,
- ii = json.length,
- j;
- for (; i < ii; i++) {
- j = json[i] || {};
- elements[has](j.type) && res.push(this[j.type]().attr(j));
- }
- }
- return res;
- };
-
- /*\
- * Raphael.format
- [ method ]
- **
- * Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument.
- **
- > Parameters
- **
- - token (string) string to format
- - … (string) rest of arguments will be treated as parameters for replacement
- = (string) formated string
- > Usage
- | var x = 10,
- | y = 20,
- | width = 40,
- | height = 50;
- | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
- | paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width));
- \*/
- R.format = function (token, params) {
- var args = R.is(params, array) ? [0][concat](params) : arguments;
- token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) {
- return args[++i] == null ? E : args[i];
- }));
- return token || E;
- };
- /*\
- * Raphael.fullfill
- [ method ]
- **
- * A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument.
- **
- > Parameters
- **
- - token (string) string to format
- - json (object) object which properties will be used as a replacement
- = (string) formated string
- > Usage
- | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
- | paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", {
- | x: 10,
- | y: 20,
- | dim: {
- | width: 40,
- | height: 50,
- | "negative width": -40
- | }
- | }));
- \*/
- R.fullfill = (function () {
- var tokenRegex = /\{([^\}]+)\}/g,
- objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
- replacer = function (all, key, obj) {
- var res = obj;
- key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) {
- name = name || quotedName;
- if (res) {
- if (name in res) {
- res = res[name];
- }
- typeof res == "function" && isFunc && (res = res());
- }
- });
- res = (res == null || res == obj ? all : res) + "";
- return res;
- };
- return function (str, obj) {
- return String(str).replace(tokenRegex, function (all, key) {
- return replacer(all, key, obj);
- });
- };
- })();
- /*\
- * Raphael.ninja
- [ method ]
- **
- * If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method.
- * Beware, that in this case plugins could stop working, because they are depending on global variable existance.
- **
- = (object) Raphael object
- > Usage
- | (function (local_raphael) {
- | var paper = local_raphael(10, 10, 320, 200);
- | …
- | })(Raphael.ninja());
- \*/
- R.ninja = function () {
- oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael;
- return R;
- };
- /*\
- * Raphael.st
- [ property (object) ]
- **
- * You can add your own method to elements and sets. It is wise to add a set method for each element method
- * you added, so you will be able to call the same method on sets too.
- **
- * See also @Raphael.el.
- > Usage
- | Raphael.el.red = function () {
- | this.attr({fill: "#f00"});
- | };
- | Raphael.st.red = function () {
- | this.forEach(function (el) {
- | el.red();
- | });
- | };
- | // then use it
- | paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red();
- \*/
- R.st = setproto;
-
- eve.on("raphael.DOMload", function () {
- loaded = true;
- });
-
- // Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
- (function (doc, loaded, f) {
- if (doc.readyState == null && doc.addEventListener){
- doc.addEventListener(loaded, f = function () {
- doc.removeEventListener(loaded, f, false);
- doc.readyState = "complete";
- }, false);
- doc.readyState = "loading";
- }
- function isLoaded() {
- (/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload");
- }
- isLoaded();
- })(document, "DOMContentLoaded");
-
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël - JavaScript Vector Library │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ SVG Module │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function(){
- if (!R.svg) {
- return;
- }
- var has = "hasOwnProperty",
- Str = String,
- toFloat = parseFloat,
- toInt = parseInt,
- math = Math,
- mmax = math.max,
- abs = math.abs,
- pow = math.pow,
- separator = /[, ]+/,
- eve = R.eve,
- E = "",
- S = " ";
- var xlink = "http://www.w3.org/1999/xlink",
- markers = {
- block: "M5,0 0,2.5 5,5z",
- classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z",
- diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z",
- open: "M6,1 1,3.5 6,6",
- oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z"
- },
- markerCounter = {};
- R.toString = function () {
- return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
- };
- var $ = function (el, attr) {
- if (attr) {
- if (typeof el == "string") {
- el = $(el);
- }
- for (var key in attr) if (attr[has](key)) {
- if (key.substring(0, 6) == "xlink:") {
- el.setAttributeNS(xlink, key.substring(6), Str(attr[key]));
- } else {
- el.setAttribute(key, Str(attr[key]));
- }
- }
- } else {
- el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el);
- el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
- }
- return el;
- },
- addGradientFill = function (element, gradient) {
- var type = "linear",
- id = element.id + gradient,
- fx = .5, fy = .5,
- o = element.node,
- SVG = element.paper,
- s = o.style,
- el = R._g.doc.getElementById(id);
- if (!el) {
- gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) {
- type = "radial";
- if (_fx && _fy) {
- fx = toFloat(_fx);
- fy = toFloat(_fy);
- var dir = ((fy > .5) * 2 - 1);
- pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
- (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
- fy != .5 &&
- (fy = fy.toFixed(5) - 1e-5 * dir);
- }
- return E;
- });
- gradient = gradient.split(/\s*\-\s*/);
- if (type == "linear") {
- var angle = gradient.shift();
- angle = -toFloat(angle);
- if (isNaN(angle)) {
- return null;
- }
- var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))],
- max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1);
- vector[2] *= max;
- vector[3] *= max;
- if (vector[2] < 0) {
- vector[0] = -vector[2];
- vector[2] = 0;
- }
- if (vector[3] < 0) {
- vector[1] = -vector[3];
- vector[3] = 0;
- }
- }
- var dots = R._parseDots(gradient);
- if (!dots) {
- return null;
- }
- id = id.replace(/[\(\)\s,\xb0#]/g, "_");
-
- if (element.gradient && id != element.gradient.id) {
- SVG.defs.removeChild(element.gradient);
- delete element.gradient;
- }
-
- if (!element.gradient) {
- el = $(type + "Gradient", {id: id});
- element.gradient = el;
- $(el, type == "radial" ? {
- fx: fx,
- fy: fy
- } : {
- x1: vector[0],
- y1: vector[1],
- x2: vector[2],
- y2: vector[3],
- gradientTransform: element.matrix.invert()
- });
- SVG.defs.appendChild(el);
- for (var i = 0, ii = dots.length; i < ii; i++) {
- el.appendChild($("stop", {
- offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%",
- "stop-color": dots[i].color || "#fff"
- }));
- }
- }
- }
- $(o, {
- fill: "url('" + document.location + "#" + id + "')",
- opacity: 1,
- "fill-opacity": 1
- });
- s.fill = E;
- s.opacity = 1;
- s.fillOpacity = 1;
- return 1;
- },
- updatePosition = function (o) {
- var bbox = o.getBBox(1);
- $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
- },
- addArrow = function (o, value, isEnd) {
- if (o.type == "path") {
- var values = Str(value).toLowerCase().split("-"),
- p = o.paper,
- se = isEnd ? "end" : "start",
- node = o.node,
- attrs = o.attrs,
- stroke = attrs["stroke-width"],
- i = values.length,
- type = "classic",
- from,
- to,
- dx,
- refX,
- attr,
- w = 3,
- h = 3,
- t = 5;
- while (i--) {
- switch (values[i]) {
- case "block":
- case "classic":
- case "oval":
- case "diamond":
- case "open":
- case "none":
- type = values[i];
- break;
- case "wide": h = 5; break;
- case "narrow": h = 2; break;
- case "long": w = 5; break;
- case "short": w = 2; break;
- }
- }
- if (type == "open") {
- w += 2;
- h += 2;
- t += 2;
- dx = 1;
- refX = isEnd ? 4 : 1;
- attr = {
- fill: "none",
- stroke: attrs.stroke
- };
- } else {
- refX = dx = w / 2;
- attr = {
- fill: attrs.stroke,
- stroke: "none"
- };
- }
- if (o._.arrows) {
- if (isEnd) {
- o._.arrows.endPath && markerCounter[o._.arrows.endPath]--;
- o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--;
- } else {
- o._.arrows.startPath && markerCounter[o._.arrows.startPath]--;
- o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--;
- }
- } else {
- o._.arrows = {};
- }
- if (type != "none") {
- var pathId = "raphael-marker-" + type,
- markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id;
- if (!R._g.doc.getElementById(pathId)) {
- p.defs.appendChild($($("path"), {
- "stroke-linecap": "round",
- d: markers[type],
- id: pathId
- }));
- markerCounter[pathId] = 1;
- } else {
- markerCounter[pathId]++;
- }
- var marker = R._g.doc.getElementById(markerId),
- use;
- if (!marker) {
- marker = $($("marker"), {
- id: markerId,
- markerHeight: h,
- markerWidth: w,
- orient: "auto",
- refX: refX,
- refY: h / 2
- });
- use = $($("use"), {
- "xlink:href": "#" + pathId,
- transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")",
- "stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4)
- });
- marker.appendChild(use);
- p.defs.appendChild(marker);
- markerCounter[markerId] = 1;
- } else {
- markerCounter[markerId]++;
- use = marker.getElementsByTagName("use")[0];
- }
- $(use, attr);
- var delta = dx * (type != "diamond" && type != "oval");
- if (isEnd) {
- from = o._.arrows.startdx * stroke || 0;
- to = R.getTotalLength(attrs.path) - delta * stroke;
- } else {
- from = delta * stroke;
- to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
- }
- attr = {};
- attr["marker-" + se] = "url(#" + markerId + ")";
- if (to || from) {
- attr.d = R.getSubpath(attrs.path, from, to);
- }
- $(node, attr);
- o._.arrows[se + "Path"] = pathId;
- o._.arrows[se + "Marker"] = markerId;
- o._.arrows[se + "dx"] = delta;
- o._.arrows[se + "Type"] = type;
- o._.arrows[se + "String"] = value;
- } else {
- if (isEnd) {
- from = o._.arrows.startdx * stroke || 0;
- to = R.getTotalLength(attrs.path) - from;
- } else {
- from = 0;
- to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
- }
- o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)});
- delete o._.arrows[se + "Path"];
- delete o._.arrows[se + "Marker"];
- delete o._.arrows[se + "dx"];
- delete o._.arrows[se + "Type"];
- delete o._.arrows[se + "String"];
- }
- for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) {
- var item = R._g.doc.getElementById(attr);
- item && item.parentNode.removeChild(item);
- }
- }
- },
- dasharray = {
- "": [0],
- "none": [0],
- "-": [3, 1],
- ".": [1, 1],
- "-.": [3, 1, 1, 1],
- "-..": [3, 1, 1, 1, 1, 1],
- ". ": [1, 3],
- "- ": [4, 3],
- "--": [8, 3],
- "- .": [4, 3, 1, 3],
- "--.": [8, 3, 1, 3],
- "--..": [8, 3, 1, 3, 1, 3]
- },
- addDashes = function (o, value, params) {
- value = dasharray[Str(value).toLowerCase()];
- if (value) {
- var width = o.attrs["stroke-width"] || "1",
- butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
- dashes = [],
- i = value.length;
- while (i--) {
- dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
- }
- $(o.node, {"stroke-dasharray": dashes.join(",")});
- }
- },
- setFillAndStroke = function (o, params) {
- var node = o.node,
- attrs = o.attrs,
- vis = node.style.visibility;
- node.style.visibility = "hidden";
- for (var att in params) {
- if (params[has](att)) {
- if (!R._availableAttrs[has](att)) {
- continue;
- }
- var value = params[att];
- attrs[att] = value;
- switch (att) {
- case "blur":
- o.blur(value);
- break;
- case "title":
- var title = node.getElementsByTagName("title");
-
- // Use the existing <title>.
- if (title.length && (title = title[0])) {
- title.firstChild.nodeValue = value;
- } else {
- title = $("title");
- var val = R._g.doc.createTextNode(value);
- title.appendChild(val);
- node.appendChild(title);
- }
- break;
- case "href":
- case "target":
- var pn = node.parentNode;
- if (pn.tagName.toLowerCase() != "a") {
- var hl = $("a");
- pn.insertBefore(hl, node);
- hl.appendChild(node);
- pn = hl;
- }
- if (att == "target") {
- pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value);
- } else {
- pn.setAttributeNS(xlink, att, value);
- }
- break;
- case "cursor":
- node.style.cursor = value;
- break;
- case "transform":
- o.transform(value);
- break;
- case "arrow-start":
- addArrow(o, value);
- break;
- case "arrow-end":
- addArrow(o, value, 1);
- break;
- case "clip-rect":
- var rect = Str(value).split(separator);
- if (rect.length == 4) {
- o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
- var el = $("clipPath"),
- rc = $("rect");
- el.id = R.createUUID();
- $(rc, {
- x: rect[0],
- y: rect[1],
- width: rect[2],
- height: rect[3]
- });
- el.appendChild(rc);
- o.paper.defs.appendChild(el);
- $(node, {"clip-path": "url(#" + el.id + ")"});
- o.clip = rc;
- }
- if (!value) {
- var path = node.getAttribute("clip-path");
- if (path) {
- var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E));
- clip && clip.parentNode.removeChild(clip);
- $(node, {"clip-path": E});
- delete o.clip;
- }
- }
- break;
- case "path":
- if (o.type == "path") {
- $(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"});
- o._.dirty = 1;
- if (o._.arrows) {
- "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
- "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
- }
- }
- break;
- case "width":
- node.setAttribute(att, value);
- o._.dirty = 1;
- if (attrs.fx) {
- att = "x";
- value = attrs.x;
- } else {
- break;
- }
- case "x":
- if (attrs.fx) {
- value = -attrs.x - (attrs.width || 0);
- }
- case "rx":
- if (att == "rx" && o.type == "rect") {
- break;
- }
- case "cx":
- node.setAttribute(att, value);
- o.pattern && updatePosition(o);
- o._.dirty = 1;
- break;
- case "height":
- node.setAttribute(att, value);
- o._.dirty = 1;
- if (attrs.fy) {
- att = "y";
- value = attrs.y;
- } else {
- break;
- }
- case "y":
- if (attrs.fy) {
- value = -attrs.y - (attrs.height || 0);
- }
- case "ry":
- if (att == "ry" && o.type == "rect") {
- break;
- }
- case "cy":
- node.setAttribute(att, value);
- o.pattern && updatePosition(o);
- o._.dirty = 1;
- break;
- case "r":
- if (o.type == "rect") {
- $(node, {rx: value, ry: value});
- } else {
- node.setAttribute(att, value);
- }
- o._.dirty = 1;
- break;
- case "src":
- if (o.type == "image") {
- node.setAttributeNS(xlink, "href", value);
- }
- break;
- case "stroke-width":
- if (o._.sx != 1 || o._.sy != 1) {
- value /= mmax(abs(o._.sx), abs(o._.sy)) || 1;
- }
- node.setAttribute(att, value);
- if (attrs["stroke-dasharray"]) {
- addDashes(o, attrs["stroke-dasharray"], params);
- }
- if (o._.arrows) {
- "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
- "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
- }
- break;
- case "stroke-dasharray":
- addDashes(o, value, params);
- break;
- case "fill":
- var isURL = Str(value).match(R._ISURL);
- if (isURL) {
- el = $("pattern");
- var ig = $("image");
- el.id = R.createUUID();
- $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
- $(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
- el.appendChild(ig);
-
- (function (el) {
- R._preload(isURL[1], function () {
- var w = this.offsetWidth,
- h = this.offsetHeight;
- $(el, {width: w, height: h});
- $(ig, {width: w, height: h});
- o.paper.safari();
- });
- })(el);
- o.paper.defs.appendChild(el);
- $(node, {fill: "url(#" + el.id + ")"});
- o.pattern = el;
- o.pattern && updatePosition(o);
- break;
- }
- var clr = R.getRGB(value);
- if (!clr.error) {
- delete params.gradient;
- delete attrs.gradient;
- !R.is(attrs.opacity, "undefined") &&
- R.is(params.opacity, "undefined") &&
- $(node, {opacity: attrs.opacity});
- !R.is(attrs["fill-opacity"], "undefined") &&
- R.is(params["fill-opacity"], "undefined") &&
- $(node, {"fill-opacity": attrs["fill-opacity"]});
- } else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) {
- if ("opacity" in attrs || "fill-opacity" in attrs) {
- var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
- if (gradient) {
- var stops = gradient.getElementsByTagName("stop");
- $(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)});
- }
- }
- attrs.gradient = value;
- attrs.fill = "none";
- break;
- }
- clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
- case "stroke":
- clr = R.getRGB(value);
- node.setAttribute(att, clr.hex);
- att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
- if (att == "stroke" && o._.arrows) {
- "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
- "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
- }
- break;
- case "gradient":
- (o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value);
- break;
- case "opacity":
- if (attrs.gradient && !attrs[has]("stroke-opacity")) {
- $(node, {"stroke-opacity": value > 1 ? value / 100 : value});
- }
- // fall
- case "fill-opacity":
- if (attrs.gradient) {
- gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
- if (gradient) {
- stops = gradient.getElementsByTagName("stop");
- $(stops[stops.length - 1], {"stop-opacity": value});
- }
- break;
- }
- default:
- att == "font-size" && (value = toInt(value, 10) + "px");
- var cssrule = att.replace(/(\-.)/g, function (w) {
- return w.substring(1).toUpperCase();
- });
- node.style[cssrule] = value;
- o._.dirty = 1;
- node.setAttribute(att, value);
- break;
- }
- }
- }
-
- tuneText(o, params);
- node.style.visibility = vis;
- },
- leading = 1.2,
- tuneText = function (el, params) {
- if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
- return;
- }
- var a = el.attrs,
- node = el.node,
- fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
-
- if (params[has]("text")) {
- a.text = params.text;
- while (node.firstChild) {
- node.removeChild(node.firstChild);
- }
- var texts = Str(params.text).split("\n"),
- tspans = [],
- tspan;
- for (var i = 0, ii = texts.length; i < ii; i++) {
- tspan = $("tspan");
- i && $(tspan, {dy: fontSize * leading, x: a.x});
- tspan.appendChild(R._g.doc.createTextNode(texts[i]));
- node.appendChild(tspan);
- tspans[i] = tspan;
- }
- } else {
- tspans = node.getElementsByTagName("tspan");
- for (i = 0, ii = tspans.length; i < ii; i++) if (i) {
- $(tspans[i], {dy: fontSize * leading, x: a.x});
- } else {
- $(tspans[0], {dy: 0});
- }
- }
- $(node, {x: a.x, y: a.y});
- el._.dirty = 1;
- var bb = el._getBBox(),
- dif = a.y - (bb.y + bb.height / 2);
- dif && R.is(dif, "finite") && $(tspans[0], {dy: dif});
- },
- getRealNode = function (node) {
- if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") {
- return node.parentNode;
- } else {
- return node;
- }
- },
- Element = function (node, svg) {
- var X = 0,
- Y = 0;
- /*\
- * Element.node
- [ property (object) ]
- **
- * Gives you a reference to the DOM object, so you can assign event handlers or just mess around.
- **
- * Note: Don’t mess with it.
- > Usage
- | // draw a circle at coordinate 10,10 with radius of 10
- | var c = paper.circle(10, 10, 10);
- | c.node.onclick = function () {
- | c.attr("fill", "red");
- | };
- \*/
- this[0] = this.node = node;
- /*\
- * Element.raphael
- [ property (object) ]
- **
- * Internal reference to @Raphael object. In case it is not available.
- > Usage
- | Raphael.el.red = function () {
- | var hsb = this.paper.raphael.rgb2hsb(this.attr("fill"));
- | hsb.h = 1;
- | this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex});
- | }
- \*/
- node.raphael = true;
- /*\
- * Element.id
- [ property (number) ]
- **
- * Unique id of the element. Especially useful when you want to listen to events of the element,
- * because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method.
- \*/
- this.id = R._oid++;
- node.raphaelid = this.id;
- this.matrix = R.matrix();
- this.realPath = null;
- /*\
- * Element.paper
- [ property (object) ]
- **
- * Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions.
- > Usage
- | Raphael.el.cross = function () {
- | this.attr({fill: "red"});
- | this.paper.path("M10,10L50,50M50,10L10,50")
- | .attr({stroke: "red"});
- | }
- \*/
- this.paper = svg;
- this.attrs = this.attrs || {};
- this._ = {
- transform: [],
- sx: 1,
- sy: 1,
- deg: 0,
- dx: 0,
- dy: 0,
- dirty: 1
- };
- !svg.bottom && (svg.bottom = this);
- /*\
- * Element.prev
- [ property (object) ]
- **
- * Reference to the previous element in the hierarchy.
- \*/
- this.prev = svg.top;
- svg.top && (svg.top.next = this);
- svg.top = this;
- /*\
- * Element.next
- [ property (object) ]
- **
- * Reference to the next element in the hierarchy.
- \*/
- this.next = null;
- },
- elproto = R.el;
-
- Element.prototype = elproto;
- elproto.constructor = Element;
-
- R._engine.path = function (pathString, SVG) {
- var el = $("path");
- SVG.canvas && SVG.canvas.appendChild(el);
- var p = new Element(el, SVG);
- p.type = "path";
- setFillAndStroke(p, {
- fill: "none",
- stroke: "#000",
- path: pathString
- });
- return p;
- };
- /*\
- * Element.rotate
- [ method ]
- **
- * Deprecated! Use @Element.transform instead.
- * Adds rotation by given angle around given point to the list of
- * transformations of the element.
- > Parameters
- - deg (number) angle in degrees
- - cx (number) #optional x coordinate of the centre of rotation
- - cy (number) #optional y coordinate of the centre of rotation
- * If cx & cy aren’t specified centre of the shape is used as a point of rotation.
- = (object) @Element
- \*/
- elproto.rotate = function (deg, cx, cy) {
- if (this.removed) {
- return this;
- }
- deg = Str(deg).split(separator);
- if (deg.length - 1) {
- cx = toFloat(deg[1]);
- cy = toFloat(deg[2]);
- }
- deg = toFloat(deg[0]);
- (cy == null) && (cx = cy);
- if (cx == null || cy == null) {
- var bbox = this.getBBox(1);
- cx = bbox.x + bbox.width / 2;
- cy = bbox.y + bbox.height / 2;
- }
- this.transform(this._.transform.concat([["r", deg, cx, cy]]));
- return this;
- };
- /*\
- * Element.scale
- [ method ]
- **
- * Deprecated! Use @Element.transform instead.
- * Adds scale by given amount relative to given point to the list of
- * transformations of the element.
- > Parameters
- - sx (number) horisontal scale amount
- - sy (number) vertical scale amount
- - cx (number) #optional x coordinate of the centre of scale
- - cy (number) #optional y coordinate of the centre of scale
- * If cx & cy aren’t specified centre of the shape is used instead.
- = (object) @Element
- \*/
- elproto.scale = function (sx, sy, cx, cy) {
- if (this.removed) {
- return this;
- }
- sx = Str(sx).split(separator);
- if (sx.length - 1) {
- sy = toFloat(sx[1]);
- cx = toFloat(sx[2]);
- cy = toFloat(sx[3]);
- }
- sx = toFloat(sx[0]);
- (sy == null) && (sy = sx);
- (cy == null) && (cx = cy);
- if (cx == null || cy == null) {
- var bbox = this.getBBox(1);
- }
- cx = cx == null ? bbox.x + bbox.width / 2 : cx;
- cy = cy == null ? bbox.y + bbox.height / 2 : cy;
- this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
- return this;
- };
- /*\
- * Element.translate
- [ method ]
- **
- * Deprecated! Use @Element.transform instead.
- * Adds translation by given amount to the list of transformations of the element.
- > Parameters
- - dx (number) horisontal shift
- - dy (number) vertical shift
- = (object) @Element
- \*/
- elproto.translate = function (dx, dy) {
- if (this.removed) {
- return this;
- }
- dx = Str(dx).split(separator);
- if (dx.length - 1) {
- dy = toFloat(dx[1]);
- }
- dx = toFloat(dx[0]) || 0;
- dy = +dy || 0;
- this.transform(this._.transform.concat([["t", dx, dy]]));
- return this;
- };
- /*\
- * Element.transform
- [ method ]
- **
- * Adds transformation to the element which is separate to other attributes,
- * i.e. translation doesn’t change `x` or `y` of the rectange. The format
- * of transformation string is similar to the path string syntax:
- | "t100,100r30,100,100s2,2,100,100r45s1.5"
- * Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for
- * scale and `m` is for matrix.
- *
- * There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`.
- *
- * So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100;
- * rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin
- * coordinates as optional parameters, the default is the centre point of the element.
- * Matrix accepts six parameters.
- > Usage
- | var el = paper.rect(10, 20, 300, 200);
- | // translate 100, 100, rotate 45°, translate -100, 0
- | el.transform("t100,100r45t-100,0");
- | // if you want you can append or prepend transformations
- | el.transform("...t50,50");
- | el.transform("s2...");
- | // or even wrap
- | el.transform("t50,50...t-50-50");
- | // to reset transformation call method with empty string
- | el.transform("");
- | // to get current value call it without parameters
- | console.log(el.transform());
- > Parameters
- - tstr (string) #optional transformation string
- * If tstr isn’t specified
- = (string) current transformation string
- * else
- = (object) @Element
- \*/
- elproto.transform = function (tstr) {
- var _ = this._;
- if (tstr == null) {
- return _.transform;
- }
- R._extractTransform(this, tstr);
-
- this.clip && $(this.clip, {transform: this.matrix.invert()});
- this.pattern && updatePosition(this);
- this.node && $(this.node, {transform: this.matrix});
-
- if (_.sx != 1 || _.sy != 1) {
- var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1;
- this.attr({"stroke-width": sw});
- }
-
- return this;
- };
- /*\
- * Element.hide
- [ method ]
- **
- * Makes element invisible. See @Element.show.
- = (object) @Element
- \*/
- elproto.hide = function () {
- !this.removed && this.paper.safari(this.node.style.display = "none");
- return this;
- };
- /*\
- * Element.show
- [ method ]
- **
- * Makes element visible. See @Element.hide.
- = (object) @Element
- \*/
- elproto.show = function () {
- !this.removed && this.paper.safari(this.node.style.display = "");
- return this;
- };
- /*\
- * Element.remove
- [ method ]
- **
- * Removes element from the paper.
- \*/
- elproto.remove = function () {
- var node = getRealNode(this.node);
- if (this.removed || !node.parentNode) {
- return;
- }
- var paper = this.paper;
- paper.__set__ && paper.__set__.exclude(this);
- eve.unbind("raphael.*.*." + this.id);
- if (this.gradient) {
- paper.defs.removeChild(this.gradient);
- }
- R._tear(this, paper);
-
- node.parentNode.removeChild(node);
-
- // Remove custom data for element
- this.removeData();
-
- for (var i in this) {
- this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
- }
- this.removed = true;
- };
- elproto._getBBox = function () {
- if (this.node.style.display == "none") {
- this.show();
- var hide = true;
- }
- var canvasHidden = false,
- containerStyle;
- if (this.paper.canvas.parentElement) {
- containerStyle = this.paper.canvas.parentElement.style;
- } //IE10+ can't find parentElement
- else if (this.paper.canvas.parentNode) {
- containerStyle = this.paper.canvas.parentNode.style;
- }
-
- if(containerStyle && containerStyle.display == "none") {
- canvasHidden = true;
- containerStyle.display = "";
- }
- var bbox = {};
- try {
- bbox = this.node.getBBox();
- } catch(e) {
- // Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix
- bbox = {
- x: this.node.clientLeft,
- y: this.node.clientTop,
- width: this.node.clientWidth,
- height: this.node.clientHeight
- }
- } finally {
- bbox = bbox || {};
- if(canvasHidden){
- containerStyle.display = "none";
- }
- }
- hide && this.hide();
- return bbox;
- };
- /*\
- * Element.attr
- [ method ]
- **
- * Sets the attributes of the element.
- > Parameters
- - attrName (string) attribute’s name
- - value (string) value
- * or
- - params (object) object of name/value pairs
- * or
- - attrName (string) attribute’s name
- * or
- - attrNames (array) in this case method returns array of current values for given attribute names
- = (object) @Element if attrsName & value or params are passed in.
- = (...) value of the attribute if only attrsName is passed in.
- = (array) array of values of the attribute if attrsNames is passed in.
- = (object) object of attributes if nothing is passed in.
- > Possible parameters
- # <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p>
- o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`.
- o clip-rect (string) comma or space separated values: x, y, width and height
- o cursor (string) CSS type of the cursor
- o cx (number) the x-axis coordinate of the center of the circle, or ellipse
- o cy (number) the y-axis coordinate of the center of the circle, or ellipse
- o fill (string) colour, gradient or image
- o fill-opacity (number)
- o font (string)
- o font-family (string)
- o font-size (number) font size in pixels
- o font-weight (string)
- o height (number)
- o href (string) URL, if specified element behaves as hyperlink
- o opacity (number)
- o path (string) SVG path string format
- o r (number) radius of the circle, ellipse or rounded corner on the rect
- o rx (number) horisontal radius of the ellipse
- o ry (number) vertical radius of the ellipse
- o src (string) image URL, only works for @Element.image element
- o stroke (string) stroke colour
- o stroke-dasharray (string) [“”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”]
- o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”]
- o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”]
- o stroke-miterlimit (number)
- o stroke-opacity (number)
- o stroke-width (number) stroke width in pixels, default is '1'
- o target (string) used with href
- o text (string) contents of the text element. Use `\n` for multiline text
- o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`”
- o title (string) will create tooltip with a given text
- o transform (string) see @Element.transform
- o width (number)
- o x (number)
- o y (number)
- > Gradients
- * Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90°
- * gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black.
- *
- * radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” –
- * gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point
- * at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses.
- > Path String
- # <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p>
- > Colour Parsing
- # <ul>
- # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
- # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
- # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
- # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
- # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
- # <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200,&nbsp;100,&nbsp;0, .5)</code>”)</li>
- # <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%,&nbsp;175%,&nbsp;0%, 50%)</code>”)</li>
- # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
- # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
- # <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li>
- # <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li>
- # <li>hsl(•••%, •••%, •••%) — same as above, but in %</li>
- # <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li>
- # <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg,&nbsp;1,&nbsp;.5)</code>” or, if you want to go fancy, “<code>hsl(240°,&nbsp;1,&nbsp;.5)</code>”</li>
- # </ul>
- \*/
- elproto.attr = function (name, value) {
- if (this.removed) {
- return this;
- }
- if (name == null) {
- var res = {};
- for (var a in this.attrs) if (this.attrs[has](a)) {
- res[a] = this.attrs[a];
- }
- res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
- res.transform = this._.transform;
- return res;
- }
- if (value == null && R.is(name, "string")) {
- if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
- return this.attrs.gradient;
- }
- if (name == "transform") {
- return this._.transform;
- }
- var names = name.split(separator),
- out = {};
- for (var i = 0, ii = names.length; i < ii; i++) {
- name = names[i];
- if (name in this.attrs) {
- out[name] = this.attrs[name];
- } else if (R.is(this.paper.customAttributes[name], "function")) {
- out[name] = this.paper.customAttributes[name].def;
- } else {
- out[name] = R._availableAttrs[name];
- }
- }
- return ii - 1 ? out : out[names[0]];
- }
- if (value == null && R.is(name, "array")) {
- out = {};
- for (i = 0, ii = name.length; i < ii; i++) {
- out[name[i]] = this.attr(name[i]);
- }
- return out;
- }
- if (value != null) {
- var params = {};
- params[name] = value;
- } else if (name != null && R.is(name, "object")) {
- params = name;
- }
- for (var key in params) {
- eve("raphael.attr." + key + "." + this.id, this, params[key]);
- }
- for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
- var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
- this.attrs[key] = params[key];
- for (var subkey in par) if (par[has](subkey)) {
- params[subkey] = par[subkey];
- }
- }
- setFillAndStroke(this, params);
- return this;
- };
- /*\
- * Element.toFront
- [ method ]
- **
- * Moves the element so it is the closest to the viewer’s eyes, on top of other elements.
- = (object) @Element
- \*/
- elproto.toFront = function () {
- if (this.removed) {
- return this;
- }
- var node = getRealNode(this.node);
- node.parentNode.appendChild(node);
- var svg = this.paper;
- svg.top != this && R._tofront(this, svg);
- return this;
- };
- /*\
- * Element.toBack
- [ method ]
- **
- * Moves the element so it is the furthest from the viewer’s eyes, behind other elements.
- = (object) @Element
- \*/
- elproto.toBack = function () {
- if (this.removed) {
- return this;
- }
- var node = getRealNode(this.node);
- var parentNode = node.parentNode;
- parentNode.insertBefore(node, parentNode.firstChild);
- R._toback(this, this.paper);
- var svg = this.paper;
- return this;
- };
- /*\
- * Element.insertAfter
- [ method ]
- **
- * Inserts current object after the given one.
- = (object) @Element
- \*/
- elproto.insertAfter = function (element) {
- if (this.removed || !element) {
- return this;
- }
-
- var node = getRealNode(this.node);
- var afterNode = getRealNode(element.node || element[element.length - 1].node);
- if (afterNode.nextSibling) {
- afterNode.parentNode.insertBefore(node, afterNode.nextSibling);
- } else {
- afterNode.parentNode.appendChild(node);
- }
- R._insertafter(this, element, this.paper);
- return this;
- };
- /*\
- * Element.insertBefore
- [ method ]
- **
- * Inserts current object before the given one.
- = (object) @Element
- \*/
- elproto.insertBefore = function (element) {
- if (this.removed || !element) {
- return this;
- }
-
- var node = getRealNode(this.node);
- var beforeNode = getRealNode(element.node || element[0].node);
- beforeNode.parentNode.insertBefore(node, beforeNode);
- R._insertbefore(this, element, this.paper);
- return this;
- };
- elproto.blur = function (size) {
- // Experimental. No Safari support. Use it on your own risk.
- var t = this;
- if (+size !== 0) {
- var fltr = $("filter"),
- blur = $("feGaussianBlur");
- t.attrs.blur = size;
- fltr.id = R.createUUID();
- $(blur, {stdDeviation: +size || 1.5});
- fltr.appendChild(blur);
- t.paper.defs.appendChild(fltr);
- t._blur = fltr;
- $(t.node, {filter: "url(#" + fltr.id + ")"});
- } else {
- if (t._blur) {
- t._blur.parentNode.removeChild(t._blur);
- delete t._blur;
- delete t.attrs.blur;
- }
- t.node.removeAttribute("filter");
- }
- return t;
- };
- R._engine.circle = function (svg, x, y, r) {
- var el = $("circle");
- svg.canvas && svg.canvas.appendChild(el);
- var res = new Element(el, svg);
- res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"};
- res.type = "circle";
- $(el, res.attrs);
- return res;
- };
- R._engine.rect = function (svg, x, y, w, h, r) {
- var el = $("rect");
- svg.canvas && svg.canvas.appendChild(el);
- var res = new Element(el, svg);
- res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"};
- res.type = "rect";
- $(el, res.attrs);
- return res;
- };
- R._engine.ellipse = function (svg, x, y, rx, ry) {
- var el = $("ellipse");
- svg.canvas && svg.canvas.appendChild(el);
- var res = new Element(el, svg);
- res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"};
- res.type = "ellipse";
- $(el, res.attrs);
- return res;
- };
- R._engine.image = function (svg, src, x, y, w, h) {
- var el = $("image");
- $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"});
- el.setAttributeNS(xlink, "href", src);
- svg.canvas && svg.canvas.appendChild(el);
- var res = new Element(el, svg);
- res.attrs = {x: x, y: y, width: w, height: h, src: src};
- res.type = "image";
- return res;
- };
- R._engine.text = function (svg, x, y, text) {
- var el = $("text");
- svg.canvas && svg.canvas.appendChild(el);
- var res = new Element(el, svg);
- res.attrs = {
- x: x,
- y: y,
- "text-anchor": "middle",
- text: text,
- "font-family": R._availableAttrs["font-family"],
- "font-size": R._availableAttrs["font-size"],
- stroke: "none",
- fill: "#000"
- };
- res.type = "text";
- setFillAndStroke(res, res.attrs);
- return res;
- };
- R._engine.setSize = function (width, height) {
- this.width = width || this.width;
- this.height = height || this.height;
- this.canvas.setAttribute("width", this.width);
- this.canvas.setAttribute("height", this.height);
- if (this._viewBox) {
- this.setViewBox.apply(this, this._viewBox);
- }
- return this;
- };
- R._engine.create = function () {
- var con = R._getContainer.apply(0, arguments),
- container = con && con.container,
- x = con.x,
- y = con.y,
- width = con.width,
- height = con.height;
- if (!container) {
- throw new Error("SVG container not found.");
- }
- var cnvs = $("svg"),
- css = "overflow:hidden;",
- isFloating;
- x = x || 0;
- y = y || 0;
- width = width || 512;
- height = height || 342;
- $(cnvs, {
- height: height,
- version: 1.1,
- width: width,
- xmlns: "http://www.w3.org/2000/svg",
- "xmlns:xlink": "http://www.w3.org/1999/xlink"
- });
- if (container == 1) {
- cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px";
- R._g.doc.body.appendChild(cnvs);
- isFloating = 1;
- } else {
- cnvs.style.cssText = css + "position:relative";
- if (container.firstChild) {
- container.insertBefore(cnvs, container.firstChild);
- } else {
- container.appendChild(cnvs);
- }
- }
- container = new R._Paper;
- container.width = width;
- container.height = height;
- container.canvas = cnvs;
- container.clear();
- container._left = container._top = 0;
- isFloating && (container.renderfix = function () {});
- container.renderfix();
- return container;
- };
- R._engine.setViewBox = function (x, y, w, h, fit) {
- eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
- var paperSize = this.getSize(),
- size = mmax(w / paperSize.width, h / paperSize.height),
- top = this.top,
- aspectRatio = fit ? "xMidYMid meet" : "xMinYMin",
- vb,
- sw;
- if (x == null) {
- if (this._vbSize) {
- size = 1;
- }
- delete this._vbSize;
- vb = "0 0 " + this.width + S + this.height;
- } else {
- this._vbSize = size;
- vb = x + S + y + S + w + S + h;
- }
- $(this.canvas, {
- viewBox: vb,
- preserveAspectRatio: aspectRatio
- });
- while (size && top) {
- sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1;
- top.attr({"stroke-width": sw});
- top._.dirty = 1;
- top._.dirtyT = 1;
- top = top.prev;
- }
- this._viewBox = [x, y, w, h, !!fit];
- return this;
- };
- /*\
- * Paper.renderfix
- [ method ]
- **
- * Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant
- * on other elements after reflow it could shift half pixel which cause for lines to lost their crispness.
- * This method fixes the issue.
- **
- Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method.
- \*/
- R.prototype.renderfix = function () {
- var cnvs = this.canvas,
- s = cnvs.style,
- pos;
- try {
- pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix();
- } catch (e) {
- pos = cnvs.createSVGMatrix();
- }
- var left = -pos.e % 1,
- top = -pos.f % 1;
- if (left || top) {
- if (left) {
- this._left = (this._left + left) % 1;
- s.left = this._left + "px";
- }
- if (top) {
- this._top = (this._top + top) % 1;
- s.top = this._top + "px";
- }
- }
- };
- /*\
- * Paper.clear
- [ method ]
- **
- * Clears the paper, i.e. removes all the elements.
- \*/
- R.prototype.clear = function () {
- R.eve("raphael.clear", this);
- var c = this.canvas;
- while (c.firstChild) {
- c.removeChild(c.firstChild);
- }
- this.bottom = this.top = null;
- (this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version));
- c.appendChild(this.desc);
- c.appendChild(this.defs = $("defs"));
- };
- /*\
- * Paper.remove
- [ method ]
- **
- * Removes the paper from the DOM.
- \*/
- R.prototype.remove = function () {
- eve("raphael.remove", this);
- this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas);
- for (var i in this) {
- this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
- }
- };
- var setproto = R.st;
- for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
- setproto[method] = (function (methodname) {
- return function () {
- var arg = arguments;
- return this.forEach(function (el) {
- el[methodname].apply(el, arg);
- });
- };
- })(method);
- }
-})();
-
-// ┌─────────────────────────────────────────────────────────────────────┐ \\
-// │ Raphaël - JavaScript Vector Library │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ VML Module │ \\
-// ├─────────────────────────────────────────────────────────────────────┤ \\
-// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
-// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
-// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
-// └─────────────────────────────────────────────────────────────────────┘ \\
-
-(function(){
- if (!R.vml) {
- return;
- }
- var has = "hasOwnProperty",
- Str = String,
- toFloat = parseFloat,
- math = Math,
- round = math.round,
- mmax = math.max,
- mmin = math.min,
- abs = math.abs,
- fillString = "fill",
- separator = /[, ]+/,
- eve = R.eve,
- ms = " progid:DXImageTransform.Microsoft",
- S = " ",
- E = "",
- map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
- bites = /([clmz]),?([^clmz]*)/gi,
- blurregexp = / progid:\S+Blur\([^\)]+\)/g,
- val = /-?[^,\s-]+/g,
- cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)",
- zoom = 21600,
- pathTypes = {path: 1, rect: 1, image: 1},
- ovalTypes = {circle: 1, ellipse: 1},
- path2vml = function (path) {
- var total = /[ahqstv]/ig,
- command = R._pathToAbsolute;
- Str(path).match(total) && (command = R._path2curve);
- total = /[clmz]/g;
- if (command == R._pathToAbsolute && !Str(path).match(total)) {
- var res = Str(path).replace(bites, function (all, command, args) {
- var vals = [],
- isMove = command.toLowerCase() == "m",
- res = map[command];
- args.replace(val, function (value) {
- if (isMove && vals.length == 2) {
- res += vals + map[command == "m" ? "l" : "L"];
- vals = [];
- }
- vals.push(round(value * zoom));
- });
- return res + vals;
- });
- return res;
- }
- var pa = command(path), p, r;
- res = [];
- for (var i = 0, ii = pa.length; i < ii; i++) {
- p = pa[i];
- r = pa[i][0].toLowerCase();
- r == "z" && (r = "x");
- for (var j = 1, jj = p.length; j < jj; j++) {
- r += round(p[j] * zoom) + (j != jj - 1 ? "," : E);
- }
- res.push(r);
- }
- return res.join(S);
- },
- compensation = function (deg, dx, dy) {
- var m = R.matrix();
- m.rotate(-deg, .5, .5);
- return {
- dx: m.x(dx, dy),
- dy: m.y(dx, dy)
- };
- },
- setCoords = function (p, sx, sy, dx, dy, deg) {
- var _ = p._,
- m = p.matrix,
- fillpos = _.fillpos,
- o = p.node,
- s = o.style,
- y = 1,
- flip = "",
- dxdy,
- kx = zoom / sx,
- ky = zoom / sy;
- s.visibility = "hidden";
- if (!sx || !sy) {
- return;
- }
- o.coordsize = abs(kx) + S + abs(ky);
- s.rotation = deg * (sx * sy < 0 ? -1 : 1);
- if (deg) {
- var c = compensation(deg, dx, dy);
- dx = c.dx;
- dy = c.dy;
- }
- sx < 0 && (flip += "x");
- sy < 0 && (flip += " y") && (y = -1);
- s.flip = flip;
- o.coordorigin = (dx * -kx) + S + (dy * -ky);
- if (fillpos || _.fillsize) {
- var fill = o.getElementsByTagName(fillString);
- fill = fill && fill[0];
- o.removeChild(fill);
- if (fillpos) {
- c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1]));
- fill.position = c.dx * y + S + c.dy * y;
- }
- if (_.fillsize) {
- fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy);
- }
- o.appendChild(fill);
- }
- s.visibility = "visible";
- };
- R.toString = function () {
- return "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version;
- };
- var addArrow = function (o, value, isEnd) {
- var values = Str(value).toLowerCase().split("-"),
- se = isEnd ? "end" : "start",
- i = values.length,
- type = "classic",
- w = "medium",
- h = "medium";
- while (i--) {
- switch (values[i]) {
- case "block":
- case "classic":
- case "oval":
- case "diamond":
- case "open":
- case "none":
- type = values[i];
- break;
- case "wide":
- case "narrow": h = values[i]; break;
- case "long":
- case "short": w = values[i]; break;
- }
- }
- var stroke = o.node.getElementsByTagName("stroke")[0];
- stroke[se + "arrow"] = type;
- stroke[se + "arrowlength"] = w;
- stroke[se + "arrowwidth"] = h;
- },
- setFillAndStroke = function (o, params) {
- // o.paper.canvas.style.display = "none";
- o.attrs = o.attrs || {};
- var node = o.node,
- a = o.attrs,
- s = node.style,
- xy,
- newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r),
- isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry),
- res = o;
-
-
- for (var par in params) if (params[has](par)) {
- a[par] = params[par];
- }
- if (newpath) {
- a.path = R._getPath[o.type](o);
- o._.dirty = 1;
- }
- params.href && (node.href = params.href);
- params.title && (node.title = params.title);
- params.target && (node.target = params.target);
- params.cursor && (s.cursor = params.cursor);
- "blur" in params && o.blur(params.blur);
- if (params.path && o.type == "path" || newpath) {
- node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path);
- o._.dirty = 1;
- if (o.type == "image") {
- o._.fillpos = [a.x, a.y];
- o._.fillsize = [a.width, a.height];
- setCoords(o, 1, 1, 0, 0, 0);
- }
- }
- "transform" in params && o.transform(params.transform);
- if (isOval) {
- var cx = +a.cx,
- cy = +a.cy,
- rx = +a.rx || +a.r || 0,
- ry = +a.ry || +a.r || 0;
- node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom));
- o._.dirty = 1;
- }
- if ("clip-rect" in params) {
- var rect = Str(params["clip-rect"]).split(separator);
- if (rect.length == 4) {
- rect[2] = +rect[2] + (+rect[0]);
- rect[3] = +rect[3] + (+rect[1]);
- var div = node.clipRect || R._g.doc.createElement("div"),
- dstyle = div.style;
- dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect);
- if (!node.clipRect) {
- dstyle.position = "absolute";
- dstyle.top = 0;
- dstyle.left = 0;
- dstyle.width = o.paper.width + "px";
- dstyle.height = o.paper.height + "px";
- node.parentNode.insertBefore(div, node);
- div.appendChild(node);
- node.clipRect = div;
- }
- }
- if (!params["clip-rect"]) {
- node.clipRect && (node.clipRect.style.clip = "auto");
- }
- }
- if (o.textpath) {
- var textpathStyle = o.textpath.style;
- params.font && (textpathStyle.font = params.font);
- params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"');
- params["font-size"] && (textpathStyle.fontSize = params["font-size"]);
- params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]);
- params["font-style"] && (textpathStyle.fontStyle = params["font-style"]);
- }
- if ("arrow-start" in params) {
- addArrow(res, params["arrow-start"]);
- }
- if ("arrow-end" in params) {
- addArrow(res, params["arrow-end"], 1);
- }
- if (params.opacity != null ||
- params["stroke-width"] != null ||
- params.fill != null ||
- params.src != null ||
- params.stroke != null ||
- params["stroke-width"] != null ||
- params["stroke-opacity"] != null ||
- params["fill-opacity"] != null ||
- params["stroke-dasharray"] != null ||
- params["stroke-miterlimit"] != null ||
- params["stroke-linejoin"] != null ||
- params["stroke-linecap"] != null) {
- var fill = node.getElementsByTagName(fillString),
- newfill = false;
- fill = fill && fill[0];
- !fill && (newfill = fill = createNode(fillString));
- if (o.type == "image" && params.src) {
- fill.src = params.src;
- }
- params.fill && (fill.on = true);
- if (fill.on == null || params.fill == "none" || params.fill === null) {
- fill.on = false;
- }
- if (fill.on && params.fill) {
- var isURL = Str(params.fill).match(R._ISURL);
- if (isURL) {
- fill.parentNode == node && node.removeChild(fill);
- fill.rotate = true;
- fill.src = isURL[1];
- fill.type = "tile";
- var bbox = o.getBBox(1);
- fill.position = bbox.x + S + bbox.y;
- o._.fillpos = [bbox.x, bbox.y];
-
- R._preload(isURL[1], function () {
- o._.fillsize = [this.offsetWidth, this.offsetHeight];
- });
- } else {
- fill.color = R.getRGB(params.fill).hex;
- fill.src = E;
- fill.type = "solid";
- if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) {
- a.fill = "none";
- a.gradient = params.fill;
- fill.rotate = false;
- }
- }
- }
- if ("fill-opacity" in params || "opacity" in params) {
- var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1);
- opacity = mmin(mmax(opacity, 0), 1);
- fill.opacity = opacity;
- if (fill.src) {
- fill.color = "none";
- }
- }
- node.appendChild(fill);
- var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]),
- newstroke = false;
- !stroke && (newstroke = stroke = createNode("stroke"));
- if ((params.stroke && params.stroke != "none") ||
- params["stroke-width"] ||
- params["stroke-opacity"] != null ||
- params["stroke-dasharray"] ||
- params["stroke-miterlimit"] ||
- params["stroke-linejoin"] ||
- params["stroke-linecap"]) {
- stroke.on = true;
- }
- (params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false);
- var strokeColor = R.getRGB(params.stroke);
- stroke.on && params.stroke && (stroke.color = strokeColor.hex);
- opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1);
- var width = (toFloat(params["stroke-width"]) || 1) * .75;
- opacity = mmin(mmax(opacity, 0), 1);
- params["stroke-width"] == null && (width = a["stroke-width"]);
- params["stroke-width"] && (stroke.weight = width);
- width && width < 1 && (opacity *= width) && (stroke.weight = 1);
- stroke.opacity = opacity;
-
- params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
- stroke.miterlimit = params["stroke-miterlimit"] || 8;
- params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round");
- if ("stroke-dasharray" in params) {
- var dasharray = {
- "-": "shortdash",
- ".": "shortdot",
- "-.": "shortdashdot",
- "-..": "shortdashdotdot",
- ". ": "dot",
- "- ": "dash",
- "--": "longdash",
- "- .": "dashdot",
- "--.": "longdashdot",
- "--..": "longdashdotdot"
- };
- stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E;
- }
- newstroke && node.appendChild(stroke);
- }
- if (res.type == "text") {
- res.paper.canvas.style.display = E;
- var span = res.paper.span,
- m = 100,
- fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/);
- s = span.style;
- a.font && (s.font = a.font);
- a["font-family"] && (s.fontFamily = a["font-family"]);
- a["font-weight"] && (s.fontWeight = a["font-weight"]);
- a["font-style"] && (s.fontStyle = a["font-style"]);
- fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10;
- s.fontSize = fontSize * m + "px";
- res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "&#60;").replace(/&/g, "&#38;").replace(/\n/g, "<br>"));
- var brect = span.getBoundingClientRect();
- res.W = a.w = (brect.right - brect.left) / m;
- res.H = a.h = (brect.bottom - brect.top) / m;
- // res.paper.canvas.style.display = "none";
- res.X = a.x;
- res.Y = a.y + res.H / 2;
-
- ("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1));
- var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"];
- for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) {
- res._.dirty = 1;
- break;
- }
-
- // text-anchor emulation
- switch (a["text-anchor"]) {
- case "start":
- res.textpath.style["v-text-align"] = "left";
- res.bbx = res.W / 2;
- break;
- case "end":
- res.textpath.style["v-text-align"] = "right";
- res.bbx = -res.W / 2;
- break;
- default:
- res.textpath.style["v-text-align"] = "center";
- res.bbx = 0;
- break;
- }
- res.textpath.style["v-text-kern"] = true;
- }
- // res.paper.canvas.style.display = E;
- },
- addGradientFill = function (o, gradient, fill) {
- o.attrs = o.attrs || {};
- var attrs = o.attrs,
- pow = Math.pow,
- opacity,
- oindex,
- type = "linear",
- fxfy = ".5 .5";
- o.attrs.gradient = gradient;
- gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) {
- type = "radial";
- if (fx && fy) {
- fx = toFloat(fx);
- fy = toFloat(fy);
- pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5);
- fxfy = fx + S + fy;
- }
- return E;
- });
- gradient = gradient.split(/\s*\-\s*/);
- if (type == "linear") {
- var angle = gradient.shift();
- angle = -toFloat(angle);
- if (isNaN(angle)) {
- return null;
- }
- }
- var dots = R._parseDots(gradient);
- if (!dots) {
- return null;
- }
- o = o.shape || o.node;
- if (dots.length) {
- o.removeChild(fill);
- fill.on = true;
- fill.method = "none";
- fill.color = dots[0].color;
- fill.color2 = dots[dots.length - 1].color;
- var clrs = [];
- for (var i = 0, ii = dots.length; i < ii; i++) {
- dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color);
- }
- fill.colors = clrs.length ? clrs.join() : "0% " + fill.color;
- if (type == "radial") {
- fill.type = "gradientTitle";
- fill.focus = "100%";
- fill.focussize = "0 0";
- fill.focusposition = fxfy;
- fill.angle = 0;
- } else {
- // fill.rotate= true;
- fill.type = "gradient";
- fill.angle = (270 - angle) % 360;
- }
- o.appendChild(fill);
- }
- return 1;
- },
- Element = function (node, vml) {
- this[0] = this.node = node;
- node.raphael = true;
- this.id = R._oid++;
- node.raphaelid = this.id;
- this.X = 0;
- this.Y = 0;
- this.attrs = {};
- this.paper = vml;
- this.matrix = R.matrix();
- this._ = {
- transform: [],
- sx: 1,
- sy: 1,
- dx: 0,
- dy: 0,
- deg: 0,
- dirty: 1,
- dirtyT: 1
- };
- !vml.bottom && (vml.bottom = this);
- this.prev = vml.top;
- vml.top && (vml.top.next = this);
- vml.top = this;
- this.next = null;
- };
- var elproto = R.el;
-
- Element.prototype = elproto;
- elproto.constructor = Element;
- elproto.transform = function (tstr) {
- if (tstr == null) {
- return this._.transform;
- }
- var vbs = this.paper._viewBoxShift,
- vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E,
- oldt;
- if (vbs) {
- oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E);
- }
- R._extractTransform(this, vbt + tstr);
- var matrix = this.matrix.clone(),
- skew = this.skew,
- o = this.node,
- split,
- isGrad = ~Str(this.attrs.fill).indexOf("-"),
- isPatt = !Str(this.attrs.fill).indexOf("url(");
- matrix.translate(1, 1);
- if (isPatt || isGrad || this.type == "image") {
- skew.matrix = "1 0 0 1";
- skew.offset = "0 0";
- split = matrix.split();
- if ((isGrad && split.noRotation) || !split.isSimple) {
- o.style.filter = matrix.toFilter();
- var bb = this.getBBox(),
- bbt = this.getBBox(1),
- dx = bb.x - bbt.x,
- dy = bb.y - bbt.y;
- o.coordorigin = (dx * -zoom) + S + (dy * -zoom);
- setCoords(this, 1, 1, dx, dy, 0);
- } else {
- o.style.filter = E;
- setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate);
- }
- } else {
- o.style.filter = E;
- skew.matrix = Str(matrix);
- skew.offset = matrix.offset();
- }
- if (oldt !== null) { // empty string value is true as well
- this._.transform = oldt;
- R._extractTransform(this, oldt);
- }
- return this;
- };
- elproto.rotate = function (deg, cx, cy) {
- if (this.removed) {
- return this;
- }
- if (deg == null) {
- return;
- }
- deg = Str(deg).split(separator);
- if (deg.length - 1) {
- cx = toFloat(deg[1]);
- cy = toFloat(deg[2]);
- }
- deg = toFloat(deg[0]);
- (cy == null) && (cx = cy);
- if (cx == null || cy == null) {
- var bbox = this.getBBox(1);
- cx = bbox.x + bbox.width / 2;
- cy = bbox.y + bbox.height / 2;
- }
- this._.dirtyT = 1;
- this.transform(this._.transform.concat([["r", deg, cx, cy]]));
- return this;
- };
- elproto.translate = function (dx, dy) {
- if (this.removed) {
- return this;
- }
- dx = Str(dx).split(separator);
- if (dx.length - 1) {
- dy = toFloat(dx[1]);
- }
- dx = toFloat(dx[0]) || 0;
- dy = +dy || 0;
- if (this._.bbox) {
- this._.bbox.x += dx;
- this._.bbox.y += dy;
- }
- this.transform(this._.transform.concat([["t", dx, dy]]));
- return this;
- };
- elproto.scale = function (sx, sy, cx, cy) {
- if (this.removed) {
- return this;
- }
- sx = Str(sx).split(separator);
- if (sx.length - 1) {
- sy = toFloat(sx[1]);
- cx = toFloat(sx[2]);
- cy = toFloat(sx[3]);
- isNaN(cx) && (cx = null);
- isNaN(cy) && (cy = null);
- }
- sx = toFloat(sx[0]);
- (sy == null) && (sy = sx);
- (cy == null) && (cx = cy);
- if (cx == null || cy == null) {
- var bbox = this.getBBox(1);
- }
- cx = cx == null ? bbox.x + bbox.width / 2 : cx;
- cy = cy == null ? bbox.y + bbox.height / 2 : cy;
-
- this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
- this._.dirtyT = 1;
- return this;
- };
- elproto.hide = function () {
- !this.removed && (this.node.style.display = "none");
- return this;
- };
- elproto.show = function () {
- !this.removed && (this.node.style.display = E);
- return this;
- };
- // Needed to fix the vml setViewBox issues
- elproto.auxGetBBox = R.el.getBBox;
- elproto.getBBox = function(){
- var b = this.auxGetBBox();
- if (this.paper && this.paper._viewBoxShift)
- {
- var c = {};
- var z = 1/this.paper._viewBoxShift.scale;
- c.x = b.x - this.paper._viewBoxShift.dx;
- c.x *= z;
- c.y = b.y - this.paper._viewBoxShift.dy;
- c.y *= z;
- c.width = b.width * z;
- c.height = b.height * z;
- c.x2 = c.x + c.width;
- c.y2 = c.y + c.height;
- return c;
- }
- return b;
- };
- elproto._getBBox = function () {
- if (this.removed) {
- return {};
- }
- return {
- x: this.X + (this.bbx || 0) - this.W / 2,
- y: this.Y - this.H,
- width: this.W,
- height: this.H
- };
- };
- elproto.remove = function () {
- if (this.removed || !this.node.parentNode) {
- return;
- }
- this.paper.__set__ && this.paper.__set__.exclude(this);
- R.eve.unbind("raphael.*.*." + this.id);
- R._tear(this, this.paper);
- this.node.parentNode.removeChild(this.node);
- this.shape && this.shape.parentNode.removeChild(this.shape);
- for (var i in this) {
- this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
- }
- this.removed = true;
- };
- elproto.attr = function (name, value) {
- if (this.removed) {
- return this;
- }
- if (name == null) {
- var res = {};
- for (var a in this.attrs) if (this.attrs[has](a)) {
- res[a] = this.attrs[a];
- }
- res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
- res.transform = this._.transform;
- return res;
- }
- if (value == null && R.is(name, "string")) {
- if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) {
- return this.attrs.gradient;
- }
- var names = name.split(separator),
- out = {};
- for (var i = 0, ii = names.length; i < ii; i++) {
- name = names[i];
- if (name in this.attrs) {
- out[name] = this.attrs[name];
- } else if (R.is(this.paper.customAttributes[name], "function")) {
- out[name] = this.paper.customAttributes[name].def;
- } else {
- out[name] = R._availableAttrs[name];
- }
- }
- return ii - 1 ? out : out[names[0]];
- }
- if (this.attrs && value == null && R.is(name, "array")) {
- out = {};
- for (i = 0, ii = name.length; i < ii; i++) {
- out[name[i]] = this.attr(name[i]);
- }
- return out;
- }
- var params;
- if (value != null) {
- params = {};
- params[name] = value;
- }
- value == null && R.is(name, "object") && (params = name);
- for (var key in params) {
- eve("raphael.attr." + key + "." + this.id, this, params[key]);
- }
- if (params) {
- for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
- var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
- this.attrs[key] = params[key];
- for (var subkey in par) if (par[has](subkey)) {
- params[subkey] = par[subkey];
- }
- }
- // this.paper.canvas.style.display = "none";
- if (params.text && this.type == "text") {
- this.textpath.string = params.text;
- }
- setFillAndStroke(this, params);
- // this.paper.canvas.style.display = E;
- }
- return this;
- };
- elproto.toFront = function () {
- !this.removed && this.node.parentNode.appendChild(this.node);
- this.paper && this.paper.top != this && R._tofront(this, this.paper);
- return this;
- };
- elproto.toBack = function () {
- if (this.removed) {
- return this;
- }
- if (this.node.parentNode.firstChild != this.node) {
- this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
- R._toback(this, this.paper);
- }
- return this;
- };
- elproto.insertAfter = function (element) {
- if (this.removed) {
- return this;
- }
- if (element.constructor == R.st.constructor) {
- element = element[element.length - 1];
- }
- if (element.node.nextSibling) {
- element.node.parentNode.insertBefore(this.node, element.node.nextSibling);
- } else {
- element.node.parentNode.appendChild(this.node);
- }
- R._insertafter(this, element, this.paper);
- return this;
- };
- elproto.insertBefore = function (element) {
- if (this.removed) {
- return this;
- }
- if (element.constructor == R.st.constructor) {
- element = element[0];
- }
- element.node.parentNode.insertBefore(this.node, element.node);
- R._insertbefore(this, element, this.paper);
- return this;
- };
- elproto.blur = function (size) {
- var s = this.node.runtimeStyle,
- f = s.filter;
- f = f.replace(blurregexp, E);
- if (+size !== 0) {
- this.attrs.blur = size;
- s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")";
- s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5));
- } else {
- s.filter = f;
- s.margin = 0;
- delete this.attrs.blur;
- }
- return this;
- };
-
- R._engine.path = function (pathString, vml) {
- var el = createNode("shape");
- el.style.cssText = cssDot;
- el.coordsize = zoom + S + zoom;
- el.coordorigin = vml.coordorigin;
- var p = new Element(el, vml),
- attr = {fill: "none", stroke: "#000"};
- pathString && (attr.path = pathString);
- p.type = "path";
- p.path = [];
- p.Path = E;
- setFillAndStroke(p, attr);
- vml.canvas.appendChild(el);
- var skew = createNode("skew");
- skew.on = true;
- el.appendChild(skew);
- p.skew = skew;
- p.transform(E);
- return p;
- };
- R._engine.rect = function (vml, x, y, w, h, r) {
- var path = R._rectPath(x, y, w, h, r),
- res = vml.path(path),
- a = res.attrs;
- res.X = a.x = x;
- res.Y = a.y = y;
- res.W = a.width = w;
- res.H = a.height = h;
- a.r = r;
- a.path = path;
- res.type = "rect";
- return res;
- };
- R._engine.ellipse = function (vml, x, y, rx, ry) {
- var res = vml.path(),
- a = res.attrs;
- res.X = x - rx;
- res.Y = y - ry;
- res.W = rx * 2;
- res.H = ry * 2;
- res.type = "ellipse";
- setFillAndStroke(res, {
- cx: x,
- cy: y,
- rx: rx,
- ry: ry
- });
- return res;
- };
- R._engine.circle = function (vml, x, y, r) {
- var res = vml.path(),
- a = res.attrs;
- res.X = x - r;
- res.Y = y - r;
- res.W = res.H = r * 2;
- res.type = "circle";
- setFillAndStroke(res, {
- cx: x,
- cy: y,
- r: r
- });
- return res;
- };
- R._engine.image = function (vml, src, x, y, w, h) {
- var path = R._rectPath(x, y, w, h),
- res = vml.path(path).attr({stroke: "none"}),
- a = res.attrs,
- node = res.node,
- fill = node.getElementsByTagName(fillString)[0];
- a.src = src;
- res.X = a.x = x;
- res.Y = a.y = y;
- res.W = a.width = w;
- res.H = a.height = h;
- a.path = path;
- res.type = "image";
- fill.parentNode == node && node.removeChild(fill);
- fill.rotate = true;
- fill.src = src;
- fill.type = "tile";
- res._.fillpos = [x, y];
- res._.fillsize = [w, h];
- node.appendChild(fill);
- setCoords(res, 1, 1, 0, 0, 0);
- return res;
- };
- R._engine.text = function (vml, x, y, text) {
- var el = createNode("shape"),
- path = createNode("path"),
- o = createNode("textpath");
- x = x || 0;
- y = y || 0;
- text = text || "";
- path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1);
- path.textpathok = true;
- o.string = Str(text);
- o.on = true;
- el.style.cssText = cssDot;
- el.coordsize = zoom + S + zoom;
- el.coordorigin = "0 0";
- var p = new Element(el, vml),
- attr = {
- fill: "#000",
- stroke: "none",
- font: R._availableAttrs.font,
- text: text
- };
- p.shape = el;
- p.path = path;
- p.textpath = o;
- p.type = "text";
- p.attrs.text = Str(text);
- p.attrs.x = x;
- p.attrs.y = y;
- p.attrs.w = 1;
- p.attrs.h = 1;
- setFillAndStroke(p, attr);
- el.appendChild(o);
- el.appendChild(path);
- vml.canvas.appendChild(el);
- var skew = createNode("skew");
- skew.on = true;
- el.appendChild(skew);
- p.skew = skew;
- p.transform(E);
- return p;
- };
- R._engine.setSize = function (width, height) {
- var cs = this.canvas.style;
- this.width = width;
- this.height = height;
- width == +width && (width += "px");
- height == +height && (height += "px");
- cs.width = width;
- cs.height = height;
- cs.clip = "rect(0 " + width + " " + height + " 0)";
- if (this._viewBox) {
- R._engine.setViewBox.apply(this, this._viewBox);
- }
- return this;
- };
- R._engine.setViewBox = function (x, y, w, h, fit) {
- R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
- var paperSize = this.getSize(),
- width = paperSize.width,
- height = paperSize.height,
- H, W;
- if (fit) {
- H = height / h;
- W = width / w;
- if (w * H < width) {
- x -= (width - w * H) / 2 / H;
- }
- if (h * W < height) {
- y -= (height - h * W) / 2 / W;
- }
- }
- this._viewBox = [x, y, w, h, !!fit];
- this._viewBoxShift = {
- dx: -x,
- dy: -y,
- scale: paperSize
- };
- this.forEach(function (el) {
- el.transform("...");
- });
- return this;
- };
- var createNode;
- R._engine.initWin = function (win) {
- var doc = win.document;
- if (doc.styleSheets.length < 31) {
- doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
- } else {
- // no more room, add to the existing one
- // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx
- doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)");
- }
- try {
- !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
- createNode = function (tagName) {
- return doc.createElement('<rvml:' + tagName + ' class="rvml">');
- };
- } catch (e) {
- createNode = function (tagName) {
- return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
- };
- }
- };
- R._engine.initWin(R._g.win);
- R._engine.create = function () {
- var con = R._getContainer.apply(0, arguments),
- container = con.container,
- height = con.height,
- s,
- width = con.width,
- x = con.x,
- y = con.y;
- if (!container) {
- throw new Error("VML container not found.");
- }
- var res = new R._Paper,
- c = res.canvas = R._g.doc.createElement("div"),
- cs = c.style;
- x = x || 0;
- y = y || 0;
- width = width || 512;
- height = height || 342;
- res.width = width;
- res.height = height;
- width == +width && (width += "px");
- height == +height && (height += "px");
- res.coordsize = zoom * 1e3 + S + zoom * 1e3;
- res.coordorigin = "0 0";
- res.span = R._g.doc.createElement("span");
- res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;";
- c.appendChild(res.span);
- cs.cssText = R.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden", width, height);
- if (container == 1) {
- R._g.doc.body.appendChild(c);
- cs.left = x + "px";
- cs.top = y + "px";
- cs.position = "absolute";
- } else {
- if (container.firstChild) {
- container.insertBefore(c, container.firstChild);
- } else {
- container.appendChild(c);
- }
- }
- res.renderfix = function () {};
- return res;
- };
- R.prototype.clear = function () {
- R.eve("raphael.clear", this);
- this.canvas.innerHTML = E;
- this.span = R._g.doc.createElement("span");
- this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
- this.canvas.appendChild(this.span);
- this.bottom = this.top = null;
- };
- R.prototype.remove = function () {
- R.eve("raphael.remove", this);
- this.canvas.parentNode.removeChild(this.canvas);
- for (var i in this) {
- this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
- }
- return true;
- };
-
- var setproto = R.st;
- for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
- setproto[method] = (function (methodname) {
- return function () {
- var arg = arguments;
- return this.forEach(function (el) {
- el[methodname].apply(el, arg);
- });
- };
- })(method);
- }
-})();
-
- // EXPOSE
- // SVG and VML are appended just before the EXPOSE line
- // Even with AMD, Raphael should be defined globally
- oldRaphael.was ? (g.win.Raphael = R) : (Raphael = R);
-
- if(typeof exports == "object"){
- module.exports = R;
- }
- return R;
-}));
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index a1a65c2d72e..520a86352f7 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -37,6 +37,7 @@ captures/
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
+.idea/dictionaries
.idea/libraries
# Keystore files
@@ -48,7 +49,7 @@ captures/
# Google Services (e.g. APIs or Firebase)
google-services.json
-#Freeline
+# Freeline
freeline.py
freeline/
freeline_project_description.json
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 31c9fb31167..4f88399d2d8 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -49,3 +49,8 @@ local.properties
# Code Recommenders
.recommenders/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 401fee15748..ec7e95c6ab5 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,6 +4,7 @@
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
+.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore
index 970d897c75c..5ed6acb6576 100644
--- a/vendor/gitignore/Global/SBT.gitignore
+++ b/vendor/gitignore/Global/SBT.gitignore
@@ -1,9 +1,12 @@
# Simple Build Tool
# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
+dist/*
target/
lib_managed/
src_managed/
project/boot/
+project/plugins/project/
.history
.cache
+.lib/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index dbb4a2dfa1a..6143e53f9e3 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,3 +1,4 @@
+# Compiled class file
*.class
# Log file
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 9af45b175ae..5f2dbe11df9 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -8,5 +8,5 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
-# Exclude maven wrapper
+# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 38ac77e405e..00cbbdf53f6 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -2,6 +2,8 @@
logs
*.log
npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
# Runtime data
pids
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index af90c007a3f..09dfede4814 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -45,10 +45,10 @@ Carthage/Build
# fastlane
#
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore
index 6d67f119175..ae5ec9fe1d9 100644
--- a/vendor/gitignore/PlayFramework.gitignore
+++ b/vendor/gitignore/PlayFramework.gitignore
@@ -5,6 +5,7 @@ bin/
/lib/
/logs/
/modules
+/project/project
/project/target
/target
tmp/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index cf3102d6b00..62c1e736924 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -76,6 +76,9 @@ target/
# celery beat schedule file
celerybeat-schedule
+# SageMath parsed files
+*.sage.py
+
# dotenv
.env
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index 006a7b247fe..9c07d4ae988 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -1,23 +1,2 @@
*.class
*.log
-
-# sbt specific
-.cache
-.history
-.lib/
-dist/*
-target/
-lib_managed/
-src_managed/
-project/boot/
-project/plugins/project/
-
-# Scala-IDE specific
-.ensime
-.ensime_cache/
-.scala_dependencies
-.worksheet
-
-# ENSIME specific
-.ensime_cache/
-.ensime
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 099d22ae2f4..d5340449396 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -59,7 +59,7 @@ Carthage/Build
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index ed4d3c6c28d..6c224e024e9 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -25,7 +25,6 @@
/bin/*
!bin/console
!bin/symfony_requirements
-/vendor/
# Assets and user uploads
/web/bundles/
@@ -38,8 +37,5 @@
# Build data
/build/
-# Composer PHAR
-/composer.phar
-
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 69bfb1eec3e..57ed9f5d972 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -28,7 +28,6 @@
*.blg
*-blx.aux
*-blx.bib
-*.brf
*.run.xml
## Build tool auxiliary files:
@@ -77,8 +76,6 @@ acs-*.bib
*.t[1-9]
*.t[1-9][0-9]
*.tfm
-*.[1-9]
-*.[1-9][0-9]
#(r)(e)ledmac/(r)(e)ledpar
*.end
@@ -134,6 +131,9 @@ acs-*.bib
*.mlf
*.mlt
*.mtc[0-9]*
+*.slf[0-9]*
+*.slt[0-9]*
+*.stc[0-9]*
# minted
_minted*
@@ -142,9 +142,6 @@ _minted*
# morewrites
*.mw
-# mylatexformat
-*.fmt
-
# nomencl
*.nlo
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 8054980d742..a752eacca7d 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -166,7 +166,7 @@ PublishScripts/
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable files
+# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
@@ -276,3 +276,12 @@ __pycache__/
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Android.gitlab-ci.yml b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
new file mode 100644
index 00000000000..5f9d54ff574
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
@@ -0,0 +1,51 @@
+# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli
+image: openjdk:8-jdk
+
+variables:
+ ANDROID_COMPILE_SDK: "25"
+ ANDROID_BUILD_TOOLS: "24.0.0"
+ ANDROID_SDK_TOOLS: "24.4.1"
+
+before_script:
+ - apt-get --quiet update --yes
+ - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
+ - wget --quiet --output-document=android-sdk.tgz https://dl.google.com/android/android-sdk_r${ANDROID_SDK_TOOLS}-linux.tgz
+ - tar --extract --gzip --file=android-sdk.tgz
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_COMPILE_SDK}
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS}
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository
+ - export ANDROID_HOME=$PWD/android-sdk-linux
+ - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
+ - chmod +x ./gradlew
+
+stages:
+ - build
+ - test
+
+build:
+ stage: build
+ script:
+ - ./gradlew assembleDebug
+ artifacts:
+ paths:
+ - app/build/outputs/
+
+unitTests:
+ stage: test
+ script:
+ - ./gradlew test
+
+functionalTests:
+ stage: test
+ script:
+ - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
+ - chmod +x android-wait-for-emulator
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK}
+ - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86
+ - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio &
+ - ./android-wait-for-emulator
+ - adb shell input keyevent 82
+ - ./gradlew cAT
diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
new file mode 100644
index 00000000000..27537689b80
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
@@ -0,0 +1,35 @@
+# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options
+
+# you can delete this line if you're not using Docker
+image: busybox:latest
+
+before_script:
+ - echo "Before script section"
+ - echo "For example you might run an update here or install a build dependency"
+ - echo "Or perhaps you might print out some debugging details"
+
+after_script:
+ - echo "After script section"
+ - echo "For example you might do some cleanup here"
+
+build1:
+ stage: build
+ script:
+ - echo "Do your build here"
+
+test1:
+ stage: test
+ script:
+ - echo "Do a test here"
+ - echo "For example run a test suite"
+
+test2:
+ stage: test
+ script:
+ - echo "Do another parallel test here"
+ - echo "For example run a lint test"
+
+deploy1:
+ stage: deploy
+ script:
+ - echo "Do your deploy here" \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index e8da49a935e..37e44735f7c 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -1,4 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/crystallang/crystal/
image: "crystallang/crystal:latest"
diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
new file mode 100644
index 00000000000..b3106863cca
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/python
+image: python:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - postgres:latest
+
+variables:
+ POSTGRES_DB: database_name
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - ~/.cache/pip/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - python -V # Print out python version for debugging
+ # Uncomment next line if your Django app needs a JS runtime:
+ # - apt-get update -q && apt-get install nodejs -yqq
+ - pip install -r requirements.txt
+
+test:
+ variables:
+ DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
+ script:
+ - python manage.py migrate
+ - python manage.py test
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 8c590579934..40648bcd3de 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -7,7 +7,7 @@ services:
build:
stage: build
script:
- - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-')
- - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
+ - export IMAGE_TAG=$(echo -en $CI_COMMIT_REF_NAME | tr -c '[:alnum:]_.-' '-')
+ - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY
- docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .
- docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG"
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index 98d3039ad06..a65e48a3389 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -6,6 +6,13 @@
# https://github.com/gradle/gradle
image: java:8
+# Disable the Gradle daemon for Continuous Integration servers as correctness
+# is usually a priority over speed in CI environments. Using a fresh
+# runtime for each build is more reliable since the runtime is completely
+# isolated from any previous builds.
+variables:
+ GRADLE_OPTS: "-Dorg.gradle.daemon=false"
+
# Make the gradle wrapper executable. This essentially downloads a copy of
# Gradle to build the project with.
# https://docs.gradle.org/current/userguide/gradle_wrapper.html
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
index 80f7b87b6c0..d6c93c6fcf7 100644
--- a/vendor/gitlab-ci-yml/LICENSE
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016 GitLab.org
+Copyright (c) 2016-2017 GitLab.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
new file mode 100644
index 00000000000..0d6a6eddc97
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
@@ -0,0 +1,78 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/php
+image: php:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+
+variables:
+ MYSQL_DATABASE: project_name
+ MYSQL_ROOT_PASSWORD: secret
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - vendor/
+ - node_modules/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ # Update packages
+ - apt-get update -yqq
+
+ # Upgrade to Node 7
+ - curl -sL https://deb.nodesource.com/setup_7.x | bash -
+
+ # Install dependencies
+ - apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
+
+ # Install php extensions
+ - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache
+
+ # Install Composer and project dependencies.
+ - curl -sS https://getcomposer.org/installer | php
+ - php composer.phar install
+
+ # Install Node dependencies.
+ # comment this out if you don't have a node dependency
+ - npm install
+
+ # Copy over testing configuration.
+ # Don't forget to set the database config in .env.testing correctly
+ # DB_HOST=mysql
+ # DB_DATABASE=project_name
+ # DB_USERNAME=root
+ # DB_PASSWORD=secret
+ - cp .env.testing .env
+
+ # Run npm build
+ # comment this out if you don't have a frontend build
+ # you can change this to to your frontend building script like
+ # npm run build
+ - npm run dev
+
+ # Generate an application key. Re-cache.
+ - php artisan key:generate
+ - php artisan config:cache
+
+ # Run database migrations.
+ - php artisan migrate
+
+ # Run database seed
+ - php artisan db:seed
+
+test:
+ script:
+ # run laravel tests
+ - php vendor/bin/phpunit --coverage-text --colors=never
+
+ # run frontend tests
+ # if you have any task for testing frontend
+ # set it in your package.json script
+ # comment this out if you don't have a frontend test
+ - npm test
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index 1678a47f9ac..91b096654d1 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -3,9 +3,9 @@
# For docker image tags see https://hub.docker.com/_/maven/
#
# For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
-#
+#
# This template will build and test your projects as well as create the documentation.
-#
+#
# * Caches downloaded dependencies and plugins between invocation.
# * Does only verify merge requests but deploy built artifacts of the
# master branch.
@@ -17,18 +17,19 @@
variables:
# This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
- MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
# `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
+# To keep cache across branches add 'key: "$CI_JOB_REF_NAME"'
cache:
paths:
- - /root/.m2/repository/
+ - .m2/repository
-# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
+# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
# Because some enforcer rules might check dependency convergence and class duplications
# we use `test-compile` here instead of `validate`, so the correct classpath is picked up.
.validate: &validate
diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
new file mode 100644
index 00000000000..d3bb388a1e7
--- /dev/null
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -0,0 +1,92 @@
+image: ayufan/openshift-cli
+
+stages:
+ - test
+ - review
+ - staging
+ - production
+ - cleanup
+
+variables:
+ OPENSHIFT_SERVER: openshift.default.svc.cluster.local
+ # OPENSHIFT_DOMAIN: apps.example.com
+ # Configure this variable in Secure Variables:
+ # OPENSHIFT_TOKEN: my.openshift.token
+
+test1:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+test2:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+.deploy: &deploy
+ before_script:
+ - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
+ - oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID"
+ script:
+ - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
+ - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
+ - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
+
+review:
+ <<: *deploy
+ stage: review
+ variables:
+ APP: $CI_COMMIT_REF_NAME
+ APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
+ on_stop: stop-review
+ only:
+ - branches
+ except:
+ - master
+
+stop-review:
+ <<: *deploy
+ stage: cleanup
+ script:
+ - oc delete all -l "app=$APP"
+ when: manual
+ variables:
+ APP: $CI_COMMIT_REF_NAME
+ GIT_STRATEGY: none
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ action: stop
+ only:
+ - branches
+ except:
+ - master
+
+staging:
+ <<: *deploy
+ stage: staging
+ variables:
+ APP: staging
+ APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ only:
+ - master
+
+production:
+ <<: *deploy
+ stage: production
+ variables:
+ APP: production
+ APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ when: manual
+ environment:
+ name: production
+ url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
deleted file mode 100644
index 2ba5cad9682..00000000000
--- a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-# This file is a template, and might need editing before it works on your project.
-image: ayufan/openshift-cli
-
-stages:
- - test
- - review
- - staging
- - production
-
-variables:
- OPENSHIFT_SERVER: openshift.default.svc.cluster.local
- # OPENSHIFT_DOMAIN: apps.example.com
- # Configure this variable in Secure Variables:
- # OPENSHIFT_TOKEN: my.openshift.token
-
-test1:
- stage: test
- before_script: []
- script:
- - echo run tests
-
-test2:
- stage: test
- before_script: []
- script:
- - echo run tests
-
-.deploy: &deploy
- before_script:
- - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
- - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME"
- script:
- - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
- - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
- - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
-
-review:
- <<: *deploy
- stage: review
- variables:
- APP: $CI_BUILD_REF_NAME
- APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
- environment:
- name: review/$CI_BUILD_REF_NAME
- url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
- on_stop: stop-review
- only:
- - branches
- except:
- - master
-
-stop-review:
- <<: *deploy
- stage: review
- script:
- - oc delete all -l "app=$APP"
- when: manual
- variables:
- APP: $CI_BUILD_REF_NAME
- GIT_STRATEGY: none
- environment:
- name: review/$CI_BUILD_REF_NAME
- action: stop
- only:
- - branches
- except:
- - master
-
-staging:
- <<: *deploy
- stage: staging
- variables:
- APP: staging
- APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
- environment:
- name: staging
- url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
- only:
- - master
-
-production:
- <<: *deploy
- stage: production
- variables:
- APP: production
- APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
- when: manual
- environment:
- name: production
- url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
- only:
- - master
diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
new file mode 100644
index 00000000000..bb8caa49d6b
--- /dev/null
+++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
@@ -0,0 +1,33 @@
+# Select image from https://hub.docker.com/_/php/
+image: php:7.1.1
+
+# Select what we should cache between builds
+cache:
+ paths:
+ - vendor/
+
+before_script:
+- apt-get update -yqq
+- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
+# Install PHP extensions
+- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
+# Install and run Composer
+- curl -sS https://getcomposer.org/installer | php
+- php composer.phar install
+
+# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# See http://docs.gitlab.com/ce/ci/services/README.html for examples.
+services:
+ - mysql:5.7
+
+# Set any variables we need
+variables:
+ # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/)
+ MYSQL_DATABASE: mysql_database
+ MYSQL_ROOT_PASSWORD: mysql_strong_password
+
+# Run our tests
+# If Xdebug was installed you can generate a coverage report and see code coverage metrics.
+test:
+ script:
+ - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
index 45df6975259..a72b8281401 100644
--- a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
@@ -9,3 +9,9 @@ pages:
- public
only:
- master
+
+test:
+ script:
+ - hugo
+ except:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
index 36918fc005a..d98cf94d635 100644
--- a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
@@ -1,11 +1,15 @@
-# Full project: https://gitlab.com/pages/jekyll
+# Template project: https://gitlab.com/pages/jekyll
+# Docs: https://docs.gitlab.com/ce/pages/
+# Jekyll version: 3.4.0
image: ruby:2.3
+before_script:
+- bundle install
+
test:
stage: test
script:
- - gem install jekyll
- - jekyll build -d test
+ - bundle exec jekyll build -d test
artifacts:
paths:
- test
@@ -15,10 +19,10 @@ test:
pages:
stage: deploy
script:
- - gem install jekyll
- - jekyll build -d public
+ - bundle exec jekyll build -d public
artifacts:
paths:
- public
only:
- master
+ \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index 7298ea73bab..c644560647f 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
- review
- staging
- production
+ - cleanup
build:
stage: build
@@ -23,12 +24,12 @@ build:
production:
stage: production
variables:
- CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
script:
- command deploy
environment:
name: production
- url: http://production.$KUBE_DOMAIN
+ url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
when: manual
only:
- master
@@ -36,24 +37,24 @@ production:
staging:
stage: staging
variables:
- CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
script:
- command deploy
environment:
name: staging
- url: http://staging.$KUBE_DOMAIN
+ url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
only:
- master
review:
stage: review
variables:
- CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
script:
- command deploy
environment:
- name: review/$CI_BUILD_REF_NAME
- url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
on_stop: stop_review
only:
- branches
@@ -61,13 +62,13 @@ review:
- master
stop_review:
- stage: review
+ stage: cleanup
variables:
GIT_STRATEGY: none
script:
- command destroy
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
only:
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
index 249adbc9f4a..27c9107e0d7 100644
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# Explaination on the scripts:
+# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/openshift-deploy
@@ -12,6 +12,7 @@ stages:
- review
- staging
- production
+ - cleanup
build:
stage: build
@@ -23,12 +24,12 @@ build:
production:
stage: production
variables:
- CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
script:
- command deploy
environment:
name: production
- url: http://production.$KUBE_DOMAIN
+ url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN
when: manual
only:
- master
@@ -36,24 +37,24 @@ production:
staging:
stage: staging
variables:
- CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
script:
- command deploy
environment:
name: staging
- url: http://staging.$KUBE_DOMAIN
+ url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN
only:
- master
review:
stage: review
variables:
- CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
script:
- command deploy
environment:
- name: review/$CI_BUILD_REF_NAME
- url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
on_stop: stop_review
only:
- branches
@@ -61,13 +62,13 @@ review:
- master
stop_review:
- stage: review
+ stage: cleanup
variables:
GIT_STRATEGY: none
script:
- command destroy
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
only:
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
new file mode 100644
index 00000000000..a2cbef126ad
--- /dev/null
+++ b/vendor/licenses.csv
@@ -0,0 +1,945 @@
+RedCloth,4.3.2,MIT
+abbrev,1.0.9,ISC
+accepts,1.3.3,MIT
+ace-rails-ap,4.1.0,MIT
+acorn,4.0.4,MIT
+acorn-dynamic-import,2.0.1,MIT
+acorn-jsx,3.0.1,MIT
+actionmailer,4.2.8,MIT
+actionpack,4.2.8,MIT
+actionview,4.2.8,MIT
+activejob,4.2.8,MIT
+activemodel,4.2.8,MIT
+activerecord,4.2.8,MIT
+activesupport,4.2.8,MIT
+acts-as-taggable-on,4.0.0,MIT
+addressable,2.3.8,Apache 2.0
+after,0.8.2,MIT
+after_commit_queue,1.3.0,MIT
+ajv,4.11.2,MIT
+ajv-keywords,1.5.1,MIT
+akismet,2.0.0,MIT
+align-text,0.1.4,MIT
+allocations,1.0.5,MIT
+amdefine,1.0.1,BSD-3-Clause OR MIT
+ansi-escapes,1.4.0,MIT
+ansi-html,0.0.7,Apache 2.0
+ansi-regex,2.1.1,MIT
+ansi-styles,2.2.1,MIT
+anymatch,1.3.0,ISC
+append-transform,0.4.0,MIT
+aproba,1.1.0,ISC
+are-we-there-yet,1.1.2,ISC
+arel,6.0.4,MIT
+argparse,1.0.9,MIT
+arr-diff,2.0.0,MIT
+arr-flatten,1.0.1,MIT
+array-find,1.0.0,MIT
+array-flatten,1.1.1,MIT
+array-slice,0.2.3,MIT
+array-union,1.0.2,MIT
+array-uniq,1.0.3,MIT
+array-unique,0.2.1,MIT
+arraybuffer.slice,0.0.6,MIT
+arrify,1.0.1,MIT
+asana,0.4.0,MIT
+asciidoctor,1.5.3,MIT
+asciidoctor-plantuml,0.0.7,MIT
+asn1,0.2.3,MIT
+asn1.js,4.9.1,MIT
+assert,1.4.1,MIT
+assert-plus,0.2.0,MIT
+async,0.2.10,MIT
+async-each,1.0.1,MIT
+asynckit,0.4.0,MIT
+attr_encrypted,3.0.3,MIT
+attr_required,1.0.0,MIT
+autoparse,0.3.3,Apache 2.0
+autoprefixer-rails,6.2.3,MIT
+aws-sign2,0.6.0,Apache 2.0
+aws4,1.6.0,MIT
+axiom-types,0.1.1,MIT
+babel-code-frame,6.22.0,MIT
+babel-core,6.23.1,MIT
+babel-generator,6.23.0,MIT
+babel-helper-bindify-decorators,6.22.0,MIT
+babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT
+babel-helper-call-delegate,6.22.0,MIT
+babel-helper-define-map,6.23.0,MIT
+babel-helper-explode-assignable-expression,6.22.0,MIT
+babel-helper-explode-class,6.22.0,MIT
+babel-helper-function-name,6.23.0,MIT
+babel-helper-get-function-arity,6.22.0,MIT
+babel-helper-hoist-variables,6.22.0,MIT
+babel-helper-optimise-call-expression,6.23.0,MIT
+babel-helper-regex,6.22.0,MIT
+babel-helper-remap-async-to-generator,6.22.0,MIT
+babel-helper-replace-supers,6.23.0,MIT
+babel-helpers,6.23.0,MIT
+babel-loader,6.2.10,MIT
+babel-messages,6.23.0,MIT
+babel-plugin-check-es2015-constants,6.22.0,MIT
+babel-plugin-istanbul,4.0.0,New BSD
+babel-plugin-syntax-async-functions,6.13.0,MIT
+babel-plugin-syntax-async-generators,6.13.0,MIT
+babel-plugin-syntax-class-properties,6.13.0,MIT
+babel-plugin-syntax-decorators,6.13.0,MIT
+babel-plugin-syntax-dynamic-import,6.18.0,MIT
+babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
+babel-plugin-syntax-object-rest-spread,6.13.0,MIT
+babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
+babel-plugin-transform-async-generator-functions,6.22.0,MIT
+babel-plugin-transform-async-to-generator,6.22.0,MIT
+babel-plugin-transform-class-properties,6.23.0,MIT
+babel-plugin-transform-decorators,6.22.0,MIT
+babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT
+babel-plugin-transform-es2015-block-scoping,6.23.0,MIT
+babel-plugin-transform-es2015-classes,6.23.0,MIT
+babel-plugin-transform-es2015-computed-properties,6.22.0,MIT
+babel-plugin-transform-es2015-destructuring,6.23.0,MIT
+babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT
+babel-plugin-transform-es2015-for-of,6.23.0,MIT
+babel-plugin-transform-es2015-function-name,6.22.0,MIT
+babel-plugin-transform-es2015-literals,6.22.0,MIT
+babel-plugin-transform-es2015-modules-amd,6.22.0,MIT
+babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT
+babel-plugin-transform-es2015-modules-umd,6.23.0,MIT
+babel-plugin-transform-es2015-object-super,6.22.0,MIT
+babel-plugin-transform-es2015-parameters,6.23.0,MIT
+babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT
+babel-plugin-transform-es2015-spread,6.22.0,MIT
+babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT
+babel-plugin-transform-es2015-template-literals,6.22.0,MIT
+babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
+babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT
+babel-plugin-transform-exponentiation-operator,6.22.0,MIT
+babel-plugin-transform-object-rest-spread,6.23.0,MIT
+babel-plugin-transform-regenerator,6.22.0,MIT
+babel-plugin-transform-strict-mode,6.22.0,MIT
+babel-preset-es2015,6.22.0,MIT
+babel-preset-stage-2,6.22.0,MIT
+babel-preset-stage-3,6.22.0,MIT
+babel-register,6.23.0,MIT
+babel-runtime,6.22.0,MIT
+babel-template,6.23.0,MIT
+babel-traverse,6.23.1,MIT
+babel-types,6.23.0,MIT
+babosa,1.0.2,MIT
+babylon,6.15.0,MIT
+backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
+base32,0.3.2,MIT
+base64-arraybuffer,0.1.5,MIT
+base64-js,1.2.0,MIT
+base64id,1.0.0,MIT
+batch,0.5.3,MIT
+bcrypt,3.1.11,MIT
+bcrypt-pbkdf,1.0.1,New BSD
+better-assert,1.0.2,MIT
+big.js,3.1.3,MIT
+binary-extensions,1.8.0,MIT
+bindata,2.3.5,ruby
+blob,0.0.4,unknown
+block-stream,0.0.9,ISC
+bluebird,3.4.7,MIT
+bn.js,4.11.6,MIT
+body-parser,1.16.0,MIT
+boom,2.10.1,New BSD
+bootstrap-sass,3.3.6,MIT
+brace-expansion,1.1.6,MIT
+braces,1.8.5,MIT
+brorand,1.0.7,MIT
+browser,2.2.0,MIT
+browserify-aes,1.0.6,MIT
+browserify-cipher,1.0.0,MIT
+browserify-des,1.0.0,MIT
+browserify-rsa,4.0.1,MIT
+browserify-sign,4.0.0,ISC
+browserify-zlib,0.1.4,MIT
+buffer,4.9.1,MIT
+buffer-shims,1.0.0,MIT
+buffer-xor,1.0.3,MIT
+builder,3.2.3,MIT
+builtin-modules,1.1.1,MIT
+builtin-status-codes,3.0.0,MIT
+bytes,2.4.0,MIT
+caller-path,0.1.0,MIT
+callsite,1.0.0,unknown
+callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+carrierwave,0.11.2,MIT
+caseless,0.11.0,Apache 2.0
+cause,0.1,MIT
+center-align,0.1.3,MIT
+chalk,1.1.3,MIT
+charlock_holmes,0.7.3,MIT
+chokidar,1.6.1,MIT
+chronic,0.10.2,MIT
+chronic_duration,0.10.6,MIT
+chunky_png,1.3.5,MIT
+cipher-base,1.0.3,MIT
+circular-json,0.3.1,MIT
+cli-cursor,1.0.2,MIT
+cli-width,2.1.0,ISC
+cliui,2.1.0,ISC
+clone,1.0.2,MIT
+co,4.6.0,MIT
+code-point-at,1.1.0,MIT
+coercible,1.0.0,MIT
+coffee-rails,4.1.1,MIT
+coffee-script,2.4.1,MIT
+coffee-script-source,1.10.0,MIT
+colors,1.1.2,MIT
+combine-lists,1.0.1,MIT
+combined-stream,1.0.5,MIT
+commander,2.9.0,MIT
+commondir,1.0.1,MIT
+component-bind,1.0.0,unknown
+component-emitter,1.2.1,MIT
+component-inherit,0.0.3,unknown
+compressible,2.0.9,MIT
+compression,1.6.2,MIT
+compression-webpack-plugin,0.3.2,MIT
+concat-map,0.0.1,MIT
+concat-stream,1.6.0,MIT
+concurrent-ruby,1.0.4,MIT
+connect,3.5.0,MIT
+connect-history-api-fallback,1.3.0,MIT
+connection_pool,2.2.1,MIT
+console-browserify,1.1.0,MIT
+console-control-strings,1.1.0,ISC
+constants-browserify,1.0.0,MIT
+contains-path,0.1.0,MIT
+content-disposition,0.5.2,MIT
+content-type,1.0.2,MIT
+convert-source-map,1.3.0,MIT
+cookie,0.3.1,MIT
+cookie-signature,1.0.6,MIT
+core-js,2.4.1,MIT
+core-util-is,1.0.2,MIT
+crack,0.4.3,MIT
+create-ecdh,4.0.0,MIT
+create-hash,1.1.2,MIT
+create-hmac,1.1.4,MIT
+creole,0.5.0,ruby
+cryptiles,2.0.5,New BSD
+crypto-browserify,3.11.0,MIT
+css_parser,1.4.1,MIT
+custom-event,1.0.1,MIT
+d,0.1.1,MIT
+d3,3.5.11,New BSD
+d3_rails,3.5.11,MIT
+dashdash,1.14.1,MIT
+date-now,0.1.4,MIT
+debug,2.6.0,MIT
+decamelize,1.2.0,MIT
+deckar01-task_list,1.0.6,MIT
+deep-extend,0.4.1,MIT
+deep-is,0.1.3,MIT
+default-require-extensions,1.0.0,MIT
+default_value_for,3.0.2,MIT
+defaults,1.0.3,MIT
+del,2.2.2,MIT
+delayed-stream,1.0.0,MIT
+delegates,1.0.0,MIT
+depd,1.1.0,MIT
+des.js,1.0.0,MIT
+descendants_tracker,0.0.4,MIT
+destroy,1.0.4,MIT
+detect-indent,4.0.0,MIT
+devise,4.2.0,MIT
+devise-two-factor,3.0.0,MIT
+di,0.0.1,MIT
+diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2"
+diffie-hellman,5.0.2,MIT
+diffy,3.1.0,MIT
+doctrine,1.5.0,BSD
+document-register-element,1.3.0,MIT
+dom-serialize,2.2.1,MIT
+domain-browser,1.1.7,MIT
+domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+doorkeeper,4.2.0,MIT
+doorkeeper-openid_connect,1.1.2,MIT
+dropzone,4.2.0,MIT
+dropzonejs-rails,0.7.2,MIT
+duplexer,0.1.1,MIT
+ecc-jsbn,0.1.1,MIT
+ee-first,1.1.1,MIT
+ejs,2.5.6,Apache 2.0
+elliptic,6.3.3,MIT
+email_reply_trimmer,0.1.6,MIT
+emoji-unicode-version,0.2.1,MIT
+emojis-list,2.1.0,MIT
+encodeurl,1.0.1,MIT
+encryptor,3.0.0,MIT
+engine.io,1.8.2,MIT
+engine.io-client,1.8.2,MIT
+engine.io-parser,1.3.2,MIT
+enhanced-resolve,3.1.0,MIT
+ent,2.2.0,MIT
+equalizer,0.0.11,MIT
+errno,0.1.4,MIT
+error-ex,1.3.0,MIT
+erubis,2.7.0,MIT
+es5-ext,0.10.12,MIT
+es6-iterator,2.0.0,MIT
+es6-map,0.1.4,MIT
+es6-promise,4.0.5,MIT
+es6-set,0.1.4,MIT
+es6-symbol,3.1.0,MIT
+es6-weak-map,2.0.1,MIT
+escape-html,1.0.3,MIT
+escape-string-regexp,1.0.5,MIT
+escape_utils,1.1.1,MIT
+escodegen,1.8.1,Simplified BSD
+escope,3.6.0,Simplified BSD
+eslint,3.15.0,MIT
+eslint-config-airbnb-base,10.0.1,MIT
+eslint-import-resolver-node,0.2.3,MIT
+eslint-import-resolver-webpack,0.8.1,MIT
+eslint-module-utils,2.0.0,MIT
+eslint-plugin-filenames,1.1.0,MIT
+eslint-plugin-import,2.2.0,MIT
+eslint-plugin-jasmine,2.2.0,MIT
+espree,3.4.0,Simplified BSD
+esprima,3.1.3,Simplified BSD
+esrecurse,4.1.0,Simplified BSD
+estraverse,4.1.1,Simplified BSD
+esutils,2.0.2,BSD
+etag,1.7.0,MIT
+eve-raphael,0.5.0,Apache 2.0
+event-emitter,0.3.4,MIT
+eventemitter3,1.2.0,MIT
+events,1.1.1,MIT
+eventsource,0.1.6,MIT
+evp_bytestokey,1.0.0,MIT
+excon,0.52.0,MIT
+execjs,2.6.0,MIT
+exit-hook,1.1.1,MIT
+expand-braces,0.1.2,MIT
+expand-brackets,0.1.5,MIT
+expand-range,1.8.2,MIT
+express,4.14.1,MIT
+expression_parser,0.9.0,MIT
+extend,3.0.0,MIT
+extglob,0.3.2,MIT
+extlib,0.9.16,MIT
+extract-zip,1.5.0,Simplified BSD
+extsprintf,1.0.2,MIT
+faraday,0.9.2,MIT
+faraday_middleware,0.10.0,MIT
+faraday_middleware-multi_json,0.0.6,MIT
+fast-levenshtein,2.0.6,MIT
+faye-websocket,0.10.0,MIT
+fd-slicer,1.0.1,MIT
+ffi,1.9.10,BSD
+figures,1.7.0,MIT
+file-entry-cache,2.0.0,MIT
+filename-regex,2.0.0,MIT
+fileset,2.0.3,MIT
+filesize,3.5.4,New BSD
+fill-range,2.2.3,MIT
+finalhandler,0.5.1,MIT
+find-cache-dir,0.1.1,MIT
+find-root,0.1.2,MIT
+find-up,2.1.0,MIT
+flat-cache,1.2.2,MIT
+flowdock,0.7.1,MIT
+fog-aws,0.11.0,MIT
+fog-core,1.42.0,MIT
+fog-google,0.5.0,MIT
+fog-json,1.0.2,MIT
+fog-local,0.3.0,MIT
+fog-openstack,0.1.6,MIT
+fog-rackspace,0.1.1,MIT
+fog-xml,0.1.2,MIT
+font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
+for-in,0.1.6,MIT
+for-own,0.1.4,MIT
+forever-agent,0.6.1,Apache 2.0
+form-data,2.1.2,MIT
+formatador,0.2.5,MIT
+forwarded,0.1.0,MIT
+fresh,0.3.0,MIT
+fs-extra,1.0.0,MIT
+fs.realpath,1.0.0,ISC
+fsevents,,unknown
+fstream,1.0.10,ISC
+fstream-ignore,1.0.5,ISC
+function-bind,1.1.0,MIT
+gauge,2.7.2,ISC
+gemnasium-gitlab-service,0.2.6,MIT
+gemojione,3.0.1,MIT
+generate-function,2.0.0,MIT
+generate-object-property,1.2.0,MIT
+get-caller-file,1.0.2,ISC
+get_process_mem,0.2.0,MIT
+getpass,0.1.6,MIT
+gitaly,0.2.1,MIT
+github-linguist,4.7.6,MIT
+github-markup,1.4.0,MIT
+gitlab-flowdock-git-hook,1.0.1,MIT
+gitlab-grit,2.8.1,MIT
+gitlab-markup,1.5.1,MIT
+gitlab_omniauth-ldap,1.2.1,MIT
+glob,7.1.1,ISC
+glob-base,0.3.0,MIT
+glob-parent,2.0.0,ISC
+globalid,0.3.7,MIT
+globals,9.14.0,MIT
+globby,5.0.0,MIT
+gollum-grit_adapter,1.0.1,MIT
+gollum-lib,4.2.1,MIT
+gollum-rugged_adapter,0.4.2,MIT
+gon,6.1.0,MIT
+google-api-client,0.8.7,Apache 2.0
+google-protobuf,3.2.0,New BSD
+googleauth,0.5.1,Apache 2.0
+graceful-fs,4.1.11,ISC
+graceful-readlink,1.0.1,MIT
+grape,0.19.1,MIT
+grape-entity,0.6.0,MIT
+grpc,1.1.2,New BSD
+gzip-size,3.0.0,MIT
+hamlit,2.6.1,MIT
+handle-thing,1.2.5,MIT
+handlebars,4.0.6,MIT
+har-validator,2.0.6,ISC
+has,1.0.1,MIT
+has-ansi,2.0.0,MIT
+has-binary,0.1.7,MIT
+has-cors,1.1.0,MIT
+has-flag,1.0.0,MIT
+has-unicode,2.0.1,ISC
+hash.js,1.0.3,MIT
+hasha,2.2.0,MIT
+hashie,3.5.5,MIT
+hawk,3.1.3,New BSD
+health_check,2.6.0,MIT
+hipchat,1.5.2,MIT
+hoek,2.16.3,New BSD
+home-or-tmp,2.0.0,MIT
+hosted-git-info,2.2.0,ISC
+hpack.js,2.1.6,MIT
+html-entities,1.2.0,MIT
+html-pipeline,1.11.0,MIT
+html2text,0.2.0,MIT
+htmlentities,4.3.4,MIT
+http,0.9.8,MIT
+http-cookie,1.0.3,MIT
+http-deceiver,1.2.7,MIT
+http-errors,1.5.1,MIT
+http-form_data,1.0.1,MIT
+http-proxy,1.16.2,MIT
+http-proxy-middleware,0.17.3,MIT
+http-signature,1.1.1,MIT
+http_parser.rb,0.6.0,MIT
+httparty,0.13.7,MIT
+httpclient,2.8.2,ruby
+https-browserify,0.0.1,MIT
+i18n,0.8.1,MIT
+ice_nine,0.11.2,MIT
+iconv-lite,0.4.15,MIT
+ieee754,1.1.8,New BSD
+ignore,3.2.2,MIT
+imurmurhash,0.1.4,MIT
+indexof,0.0.1,unknown
+inflight,1.0.6,ISC
+influxdb,0.2.3,MIT
+inherits,2.0.3,ISC
+ini,1.3.4,ISC
+inquirer,0.12.0,MIT
+interpret,1.0.1,MIT
+invariant,2.2.2,New BSD
+invert-kv,1.0.0,MIT
+ipaddr.js,1.2.0,MIT
+ipaddress,0.8.3,MIT
+is-absolute,0.2.6,MIT
+is-arrayish,0.2.1,MIT
+is-binary-path,1.0.1,MIT
+is-buffer,1.1.4,MIT
+is-builtin-module,1.0.0,MIT
+is-dotfile,1.0.2,MIT
+is-equal-shallow,0.1.3,MIT
+is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
+is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
+is-glob,2.0.1,MIT
+is-my-json-valid,2.15.0,MIT
+is-number,2.1.0,MIT
+is-path-cwd,1.0.0,MIT
+is-path-in-cwd,1.0.0,MIT
+is-path-inside,1.0.0,MIT
+is-posix-bracket,0.1.1,MIT
+is-primitive,2.0.0,MIT
+is-property,1.0.2,MIT
+is-relative,0.2.1,MIT
+is-resolvable,1.0.0,MIT
+is-stream,1.1.0,MIT
+is-typedarray,1.0.0,MIT
+is-unc-path,0.1.2,MIT
+is-utf8,0.2.1,MIT
+is-windows,0.2.0,MIT
+isarray,1.0.0,MIT
+isbinaryfile,3.0.2,MIT
+isexe,1.1.2,ISC
+isobject,2.1.0,MIT
+isstream,0.1.2,MIT
+istanbul,0.4.5,New BSD
+istanbul-api,1.1.1,New BSD
+istanbul-lib-coverage,1.0.1,New BSD
+istanbul-lib-hook,1.0.0,New BSD
+istanbul-lib-instrument,1.4.2,New BSD
+istanbul-lib-report,1.0.0-alpha.3,New BSD
+istanbul-lib-source-maps,1.1.0,New BSD
+istanbul-reports,1.0.1,New BSD
+jasmine-core,2.5.2,MIT
+jasmine-jquery,2.1.1,MIT
+jira-ruby,1.1.2,MIT
+jodid25519,1.0.2,MIT
+jquery,2.2.1,MIT
+jquery-atwho-rails,1.3.2,MIT
+jquery-rails,4.1.1,MIT
+jquery-ujs,1.2.1,MIT
+js-cookie,2.1.3,MIT
+js-tokens,3.0.1,MIT
+js-yaml,3.8.1,MIT
+jsbn,0.1.0,BSD
+jsesc,1.3.0,MIT
+json,1.8.6,ruby
+json-jwt,1.7.1,MIT
+json-loader,0.5.4,MIT
+json-schema,0.2.3,"AFLv2.1,BSD"
+json-stable-stringify,1.0.1,MIT
+json-stringify-safe,5.0.1,ISC
+json3,3.3.2,MIT
+json5,0.5.1,MIT
+jsonfile,2.4.0,MIT
+jsonify,0.0.0,Public Domain
+jsonpointer,4.0.1,MIT
+jsprim,1.3.1,MIT
+jwt,1.5.6,MIT
+kaminari,0.17.0,MIT
+karma,1.4.1,MIT
+karma-coverage-istanbul-reporter,0.2.0,MIT
+karma-jasmine,1.1.0,MIT
+karma-mocha-reporter,2.2.2,MIT
+karma-phantomjs-launcher,1.0.2,MIT
+karma-sourcemap-loader,0.3.7,MIT
+karma-webpack,2.0.2,MIT
+kew,0.7.0,Apache 2.0
+kgio,2.10.0,LGPL-2.1+
+kind-of,3.1.0,MIT
+klaw,1.3.1,MIT
+kubeclient,2.2.0,MIT
+launchy,2.4.3,ISC
+lazy-cache,1.0.4,MIT
+lcid,1.0.0,MIT
+levn,0.3.0,MIT
+licensee,8.7.0,MIT
+little-plugger,1.1.4,MIT
+load-json-file,1.1.0,MIT
+loader-runner,2.3.0,MIT
+loader-utils,0.2.16,MIT
+locate-path,2.0.0,MIT
+lodash,4.17.4,MIT
+lodash._baseget,3.7.2,MIT
+lodash._topath,3.8.1,MIT
+lodash.camelcase,4.1.1,MIT
+lodash.capitalize,4.2.1,MIT
+lodash.cond,4.5.2,MIT
+lodash.deburr,4.1.0,MIT
+lodash.get,3.7.0,MIT
+lodash.isarray,3.0.4,MIT
+lodash.kebabcase,4.0.1,MIT
+lodash.snakecase,4.0.1,MIT
+lodash.words,4.2.0,MIT
+log4js,0.6.38,Apache 2.0
+logging,2.1.0,MIT
+longest,1.0.1,MIT
+loofah,2.0.3,MIT
+loose-envify,1.3.1,MIT
+lru-cache,2.2.4,MIT
+mail,2.6.4,MIT
+mail_room,0.9.1,MIT
+media-typer,0.3.0,MIT
+memoist,0.15.0,MIT
+memory-fs,0.4.1,MIT
+merge-descriptors,1.0.1,MIT
+method_source,0.8.2,MIT
+methods,1.1.2,MIT
+micromatch,2.3.11,MIT
+miller-rabin,4.0.0,MIT
+mime,1.3.4,MIT
+mime-db,1.26.0,MIT
+mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
+mimemagic,0.3.0,MIT
+mini_portile2,2.1.0,MIT
+minimalistic-assert,1.0.0,ISC
+minimatch,3.0.3,ISC
+minimist,0.0.8,MIT
+mkdirp,0.5.1,MIT
+moment,2.17.1,MIT
+mousetrap,1.4.6,Apache 2.0
+mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.2,MIT
+multi_json,1.12.1,MIT
+multi_xml,0.6.0,MIT
+multipart-post,2.0.0,MIT
+mustermann,0.4.0,MIT
+mustermann-grape,0.4.0,MIT
+mute-stream,0.0.5,ISC
+nan,2.5.1,MIT
+natural-compare,1.4.0,MIT
+negotiator,0.6.1,MIT
+net-ldap,0.12.1,MIT
+net-ssh,3.0.1,MIT
+netrc,0.11.0,MIT
+node-libs-browser,2.0.0,MIT
+node-pre-gyp,0.6.33,New BSD
+node-zopfli,2.0.2,MIT
+nokogiri,1.6.8.1,MIT
+nopt,3.0.6,ISC
+normalize-package-data,2.3.5,Simplified BSD
+normalize-path,2.0.1,MIT
+npmlog,4.0.2,ISC
+number-is-nan,1.0.1,MIT
+numerizer,0.1.1,MIT
+oauth,0.5.1,MIT
+oauth-sign,0.8.2,Apache 2.0
+oauth2,1.2.0,MIT
+object-assign,4.1.1,MIT
+object-component,0.0.3,unknown
+object.omit,2.0.1,MIT
+obuf,1.1.1,MIT
+octokit,4.6.2,MIT
+oj,2.17.4,MIT
+omniauth,1.4.2,MIT
+omniauth-auth0,1.4.1,MIT
+omniauth-authentiq,0.3.0,MIT
+omniauth-azure-oauth2,0.0.6,MIT
+omniauth-cas3,1.1.3,MIT
+omniauth-facebook,4.0.0,MIT
+omniauth-github,1.1.2,MIT
+omniauth-gitlab,1.0.2,MIT
+omniauth-google-oauth2,0.4.1,MIT
+omniauth-kerberos,0.3.0,MIT
+omniauth-multipassword,0.4.2,MIT
+omniauth-oauth,1.1.0,MIT
+omniauth-oauth2,1.3.1,MIT
+omniauth-oauth2-generic,0.2.2,MIT
+omniauth-saml,1.7.0,MIT
+omniauth-shibboleth,1.2.1,MIT
+omniauth-twitter,1.2.1,MIT
+omniauth_crowd,2.2.3,MIT
+on-finished,2.3.0,MIT
+on-headers,1.0.1,MIT
+once,1.3.3,ISC
+onetime,1.1.0,MIT
+opener,1.4.3,(WTFPL OR MIT)
+opn,4.0.2,MIT
+optimist,0.6.1,MIT/X11
+optionator,0.8.2,MIT
+options,0.0.6,MIT
+org-ruby,0.9.12,MIT
+original,1.0.0,MIT
+orm_adapter,0.5.0,MIT
+os,0.9.6,MIT
+os-browserify,0.2.1,MIT
+os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
+os-tmpdir,1.0.2,MIT
+p-limit,1.1.0,MIT
+p-locate,2.0.0,MIT
+pako,0.2.9,MIT
+paranoia,2.2.0,MIT
+parse-asn1,5.0.0,ISC
+parse-glob,3.0.4,MIT
+parse-json,2.2.0,MIT
+parsejson,0.0.3,MIT
+parseqs,0.0.5,MIT
+parseuri,0.0.5,MIT
+parseurl,1.3.1,MIT
+path-browserify,0.0.0,MIT
+path-exists,3.0.0,MIT
+path-is-absolute,1.0.1,MIT
+path-is-inside,1.0.2,(WTFPL OR MIT)
+path-parse,1.0.5,MIT
+path-to-regexp,0.1.7,MIT
+path-type,1.1.0,MIT
+pbkdf2,3.0.9,MIT
+pend,1.2.0,MIT
+pg,0.18.4,"BSD,ruby,GPL"
+phantomjs-prebuilt,2.1.14,Apache 2.0
+pify,2.3.0,MIT
+pikaday,1.5.1,"BSD,MIT"
+pinkie,2.0.4,MIT
+pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
+pkg-up,1.0.0,MIT
+pluralize,1.2.1,MIT
+portfinder,1.0.13,MIT
+posix-spawn,0.3.11,"MIT,LGPL"
+prelude-ls,1.1.2,MIT
+premailer,1.8.6,New BSD
+premailer-rails,1.9.2,MIT
+preserve,0.2.0,MIT
+private,0.1.7,MIT
+process,0.11.9,MIT
+process-nextick-args,1.0.7,MIT
+progress,1.1.8,MIT
+proxy-addr,1.1.3,MIT
+prr,0.0.0,MIT
+public-encrypt,4.0.0,MIT
+punycode,1.4.1,MIT
+pyu-ruby-sasl,0.0.3.3,MIT
+qjobs,1.1.5,MIT
+qs,6.2.0,New BSD
+querystring,0.2.0,MIT
+querystring-es3,0.2.1,MIT
+querystringify,0.0.4,MIT
+rack,1.6.5,MIT
+rack-accept,0.4.5,MIT
+rack-attack,4.4.1,MIT
+rack-cors,0.4.0,MIT
+rack-oauth2,1.2.3,MIT
+rack-protection,1.5.3,MIT
+rack-proxy,0.6.0,MIT
+rack-test,0.6.3,MIT
+rails,4.2.8,MIT
+rails-deprecated_sanitizer,1.0.3,MIT
+rails-dom-testing,1.0.8,MIT
+rails-html-sanitizer,1.0.3,MIT
+railties,4.2.8,MIT
+rainbow,2.1.0,MIT
+raindrops,0.17.0,LGPL-2.1+
+rake,10.5.0,MIT
+randomatic,1.1.6,MIT
+randombytes,2.0.3,MIT
+range-parser,1.2.0,MIT
+raphael,2.2.7,MIT
+raw-body,2.2.0,MIT
+raw-loader,0.5.1,MIT
+rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0)
+rdoc,4.2.2,ruby
+read-pkg,1.1.0,MIT
+read-pkg-up,1.0.1,MIT
+readable-stream,2.1.5,MIT
+readdirp,2.1.0,MIT
+readline2,1.0.1,MIT
+recaptcha,3.0.0,MIT
+rechoir,0.6.2,MIT
+recursive-open-struct,1.0.0,MIT
+redcarpet,3.4.0,MIT
+redis,3.2.2,MIT
+redis-actionpack,5.0.1,MIT
+redis-activesupport,5.0.1,MIT
+redis-namespace,1.5.2,MIT
+redis-rack,1.6.0,MIT
+redis-rails,5.0.1,MIT
+redis-store,1.2.0,MIT
+regenerate,1.3.2,MIT
+regenerator-runtime,0.10.1,MIT
+regenerator-transform,0.9.8,BSD
+regex-cache,0.4.3,MIT
+regexpu-core,2.0.0,MIT
+regjsgen,0.2.0,MIT
+regjsparser,0.1.5,BSD
+repeat-element,1.1.2,MIT
+repeat-string,1.6.1,MIT
+repeating,2.0.1,MIT
+request,2.79.0,Apache 2.0
+request-progress,2.0.1,MIT
+request_store,1.3.1,MIT
+require-directory,2.1.1,MIT
+require-main-filename,1.0.1,ISC
+require-uncached,1.0.3,MIT
+requires-port,1.0.0,MIT
+resolve,1.2.0,MIT
+resolve-from,1.0.1,MIT
+responders,2.3.0,MIT
+rest-client,2.0.0,MIT
+restore-cursor,1.0.1,MIT
+retriable,1.4.1,MIT
+right-align,0.1.3,MIT
+rimraf,2.5.4,ISC
+rinku,2.0.0,ISC
+ripemd160,1.0.1,New BSD
+rotp,2.1.2,MIT
+rouge,2.0.7,MIT
+rqrcode,0.7.0,MIT
+rqrcode-rails3,0.1.7,MIT
+ruby-fogbugz,0.2.1,MIT
+ruby-prof,0.16.2,Simplified BSD
+ruby-saml,1.4.1,MIT
+rubyntlm,0.5.2,MIT
+rubypants,0.2.0,BSD
+rufus-scheduler,3.1.10,MIT
+rugged,0.24.0,MIT
+run-async,0.1.0,MIT
+rx-lite,3.1.2,Apache 2.0
+safe-buffer,5.0.1,MIT
+safe_yaml,1.0.4,MIT
+sanitize,2.1.0,MIT
+sass,3.4.22,MIT
+sass-rails,5.0.6,MIT
+sawyer,0.8.1,MIT
+securecompare,1.0.0,MIT
+seed-fu,2.3.6,MIT
+select-hose,2.0.0,MIT
+select2,3.5.2-browserify,unknown
+select2-rails,3.5.9.3,MIT
+semver,5.3.0,ISC
+send,0.14.2,MIT
+sentry-raven,2.0.2,Apache 2.0
+serve-index,1.8.0,MIT
+serve-static,1.11.2,MIT
+set-blocking,2.0.0,ISC
+set-immediate-shim,1.0.1,MIT
+setimmediate,1.0.5,MIT
+setprototypeof,1.0.2,ISC
+settingslogic,2.0.9,MIT
+sha.js,2.4.8,MIT
+shelljs,0.7.6,New BSD
+sidekiq,4.2.7,LGPL
+sidekiq-cron,0.4.4,MIT
+sidekiq-limit_fetch,3.4.0,MIT
+signal-exit,3.0.2,ISC
+signet,0.7.3,Apache 2.0
+slack-notifier,1.5.1,MIT
+slash,1.0.0,MIT
+slice-ansi,0.0.4,MIT
+sntp,1.0.9,BSD
+socket.io,1.7.2,MIT
+socket.io-adapter,0.5.0,MIT
+socket.io-client,1.7.2,MIT
+socket.io-parser,2.3.1,MIT
+sockjs,0.3.18,MIT
+sockjs-client,1.1.1,MIT
+source-list-map,0.1.8,MIT
+source-map,0.5.6,New BSD
+source-map-support,0.4.11,MIT
+spdx-correct,1.0.2,Apache 2.0
+spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0)
+spdx-license-ids,1.2.2,Unlicense
+spdy,3.4.4,MIT
+spdy-transport,2.0.18,MIT
+sprintf-js,1.0.3,New BSD
+sprockets,3.7.1,MIT
+sprockets-rails,3.2.0,MIT
+sshpk,1.10.2,MIT
+state_machines,0.4.0,MIT
+state_machines-activemodel,0.4.0,MIT
+state_machines-activerecord,0.4.0,MIT
+stats-webpack-plugin,0.4.3,MIT
+statuses,1.3.1,MIT
+stream-browserify,2.0.1,MIT
+stream-http,2.6.3,MIT
+string-width,1.0.2,MIT
+string.fromcodepoint,0.2.1,MIT
+string.prototype.codepointat,0.2.0,MIT
+string_decoder,0.10.31,MIT
+stringex,2.5.2,MIT
+stringstream,0.0.5,MIT
+strip-ansi,3.0.1,MIT
+strip-bom,2.0.0,MIT
+strip-json-comments,1.0.4,MIT
+supports-color,0.2.0,MIT
+sys-filesystem,1.1.6,Artistic 2.0
+table,3.8.3,New BSD
+tapable,0.2.6,MIT
+tar,2.2.1,ISC
+tar-pack,3.3.0,Simplified BSD
+temple,0.7.7,MIT
+test-exclude,4.0.0,ISC
+text-table,0.2.0,MIT
+thor,0.19.4,MIT
+thread_safe,0.3.6,Apache 2.0
+throttleit,1.0.0,MIT
+through,2.3.8,MIT
+tilt,2.0.6,MIT
+timeago.js,2.0.5,MIT
+timers-browserify,2.0.2,MIT
+timfel-krb5-auth,0.8.3,LGPL
+tmp,0.0.28,MIT
+to-array,0.1.4,MIT
+to-arraybuffer,1.0.1,MIT
+to-fast-properties,1.0.2,MIT
+tool,0.2.3,MIT
+tough-cookie,2.3.2,New BSD
+trim-right,1.0.1,MIT
+truncato,0.7.8,MIT
+tryit,1.0.3,MIT
+tty-browserify,0.0.0,MIT
+tunnel-agent,0.4.3,Apache 2.0
+tweetnacl,0.14.5,Unlicense
+type-check,0.3.2,MIT
+type-is,1.6.14,MIT
+typedarray,0.0.6,MIT
+tzinfo,1.2.2,MIT
+u2f,0.2.1,MIT
+uglifier,2.7.2,MIT
+uglify-js,2.7.5,Simplified BSD
+uglify-to-browserify,1.0.2,MIT
+uid-number,0.0.6,ISC
+ultron,1.0.2,MIT
+unc-path-regex,0.1.2,MIT
+underscore,1.8.3,MIT
+underscore-rails,1.8.3,MIT
+unf,0.1.4,BSD
+unf_ext,0.0.7.2,MIT
+unicorn,5.1.0,ruby
+unicorn-worker-killer,0.4.4,ruby
+unpipe,1.0.0,MIT
+url,0.11.0,MIT
+url-parse,1.0.5,MIT
+url_safe_base64,0.2.2,MIT
+user-home,2.0.0,MIT
+useragent,2.1.12,MIT
+util,0.10.3,MIT
+util-deprecate,1.0.2,MIT
+utils-merge,1.0.0,MIT
+uuid,3.0.1,MIT
+validate-npm-package-license,3.0.1,Apache 2.0
+validates_hostname,1.0.6,MIT
+vary,1.1.0,MIT
+verror,1.3.6,MIT
+version_sorter,2.1.0,MIT
+virtus,1.0.5,MIT
+vm-browserify,0.0.4,MIT
+vmstat,2.3.0,MIT
+void-elements,2.0.1,MIT
+vue,2.1.10,MIT
+vue-resource,0.9.3,MIT
+warden,1.2.6,MIT
+watchpack,1.2.1,MIT
+wbuf,1.7.2,MIT
+webpack,2.2.1,MIT
+webpack-bundle-analyzer,2.3.0,MIT
+webpack-dev-middleware,1.10.0,MIT
+webpack-dev-server,2.3.0,MIT
+webpack-rails,0.9.9,MIT
+webpack-sources,0.1.4,MIT
+websocket-driver,0.6.5,MIT
+websocket-extensions,0.1.1,MIT
+which,1.2.12,ISC
+which-module,1.0.0,ISC
+wide-align,1.1.0,ISC
+wikicloth,0.8.1,MIT
+window-size,0.1.0,MIT
+wordwrap,0.0.2,MIT/X11
+wrap-ansi,2.1.0,MIT
+wrappy,1.0.2,ISC
+write,0.2.1,MIT
+ws,1.1.1,MIT
+wtf-8,1.0.0,MIT
+xmlhttprequest-ssl,1.5.3,MIT
+xtend,4.0.1,MIT
+y18n,3.2.1,ISC
+yargs,3.10.0,MIT
+yargs-parser,4.2.1,ISC
+yauzl,2.4.1,MIT
+yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index ad4b5223d60..2500ddc6f6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25,7 +25,7 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4:
+acorn@4.0.4, acorn@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
@@ -33,6 +33,10 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+acorn@^4.0.11, acorn@^4.0.3:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -469,6 +473,13 @@ babel-plugin-transform-decorators@^6.22.0:
babel-template "^6.22.0"
babel-types "^6.22.0"
+babel-plugin-transform-define@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+ dependencies:
+ lodash.get "4.4.2"
+ traverse "0.6.6"
+
babel-plugin-transform-es2015-arrow-functions@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -545,17 +556,17 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-amd@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21"
+babel-plugin-transform-es2015-modules-amd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
dependencies:
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.22.0"
-babel-plugin-transform-es2015-modules-commonjs@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.23.0.tgz#cba7aa6379fb7ec99250e6d46de2973aaffa7b92"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
dependencies:
babel-plugin-transform-strict-mode "^6.22.0"
babel-runtime "^6.22.0"
@@ -570,11 +581,11 @@ babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
babel-runtime "^6.22.0"
babel-template "^6.23.0"
-babel-plugin-transform-es2015-modules-umd@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.23.0.tgz#8d284ae2e19ed8fe21d2b1b26d6e7e0fcd94f0f1"
+babel-plugin-transform-es2015-modules-umd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
dependencies:
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.23.0"
@@ -665,9 +676,9 @@ babel-plugin-transform-strict-mode@^6.22.0:
babel-runtime "^6.22.0"
babel-types "^6.22.0"
-babel-preset-es2015@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835"
+babel-preset-es2015@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-transform-es2015-arrow-functions "^6.22.0"
@@ -680,10 +691,10 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-for-of "^6.22.0"
babel-plugin-transform-es2015-function-name "^6.22.0"
babel-plugin-transform-es2015-literals "^6.22.0"
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
- babel-plugin-transform-es2015-modules-umd "^6.22.0"
+ babel-plugin-transform-es2015-modules-umd "^6.24.0"
babel-plugin-transform-es2015-object-super "^6.22.0"
babel-plugin-transform-es2015-parameters "^6.22.0"
babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
@@ -694,6 +705,27 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
+babel-preset-es2016@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+ dependencies:
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+
+babel-preset-es2017@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+
+babel-preset-latest@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+ dependencies:
+ babel-preset-es2015 "^6.24.0"
+ babel-preset-es2016 "^6.22.0"
+ babel-preset-es2017 "^6.22.0"
+
babel-preset-stage-2@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
@@ -1209,7 +1241,7 @@ cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
-core-js@^2.2.0, core-js@^2.4.0:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -1391,6 +1423,10 @@ doctrine@1.5.0, doctrine@^1.2.2:
esutils "^2.0.2"
isarray "^1.0.0"
+document-register-element@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
+
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@@ -1408,6 +1444,10 @@ dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
+duplexer@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -1418,6 +1458,10 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ejs@^2.5.5:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
+
elliptic@^6.0.0:
version "6.3.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
@@ -1427,6 +1471,10 @@ elliptic@^6.0.0:
hash.js "^1.0.0"
inherits "^2.0.1"
+emoji-unicode-version@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc"
+
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@@ -1533,7 +1581,7 @@ es6-map@^0.1.3:
es6-symbol "~3.1.0"
event-emitter "~0.3.4"
-es6-promise@^4.0.5, es6-promise@~4.0.3:
+es6-promise@~4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
@@ -1734,6 +1782,10 @@ etag@~1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
+eve-raphael@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
+
event-emitter@~0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5"
@@ -1792,7 +1844,7 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
-express@^4.13.3:
+express@^4.13.3, express@^4.14.1:
version "4.14.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
dependencies:
@@ -1893,6 +1945,10 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
+filesize@^3.5.4:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
+
fill-range@^2.1.0:
version "2.2.3"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
@@ -2118,6 +2174,12 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+gzip-size@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+ dependencies:
+ duplexer "^0.1.1"
+
handle-thing@^1.2.4:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -2617,17 +2679,13 @@ jodid25519@^1.0.0:
dependencies:
jsbn "~0.1.0"
-"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4":
- version "1.11.4"
- resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a"
-
jquery-ujs@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
dependencies:
jquery ">=1.8.0"
-jquery@^2.2.1, jquery@>=1.8.0:
+jquery@>=1.8.0, jquery@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
@@ -2824,7 +2882,7 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
+loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies:
@@ -2870,6 +2928,10 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
+lodash.get@4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
lodash.get@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -2903,7 +2965,7 @@ lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -3216,6 +3278,10 @@ onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+opener@^1.4.2:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+
opn@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95"
@@ -3532,6 +3598,12 @@ range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+raphael@^2.2.7:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.2.7.tgz#231b19141f8d086986d8faceb66f8b562ee2c810"
+ dependencies:
+ eve-raphael "0.5.0"
+
raw-body@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
@@ -3540,6 +3612,10 @@ raw-body@~2.2.0:
iconv-lite "0.4.15"
unpipe "1.0.0"
+raw-loader@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
+
rc@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
@@ -3964,7 +4040,7 @@ source-map-support@^0.4.2:
dependencies:
source-map "^0.5.3"
-source-map@0.1.x, source-map@^0.1.41:
+source-map@^0.1.41:
version "0.1.43"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
dependencies:
@@ -4227,6 +4303,10 @@ tough-cookie@~2.3.0:
dependencies:
punycode "^1.4.1"
+traverse@0.6.6:
+ version "0.6.6"
+ resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -4384,9 +4464,9 @@ vue-resource@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
-vue@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"
+vue@^2.1.10:
+ version "2.1.10"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
watchpack@^1.2.0:
version "1.2.1"
@@ -4402,6 +4482,21 @@ wbuf@^1.1.0, wbuf@^1.4.0:
dependencies:
minimalistic-assert "^1.0.0"
+webpack-bundle-analyzer@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4"
+ dependencies:
+ acorn "^4.0.11"
+ chalk "^1.1.3"
+ commander "^2.9.0"
+ ejs "^2.5.5"
+ express "^4.14.1"
+ filesize "^3.5.4"
+ gzip-size "^3.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ opener "^1.4.2"
+
webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8"